@hotmeshio/hotmesh 0.0.9 → 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 (59) hide show
  1. package/README.md +84 -77
  2. package/build/modules/errors.d.ts +17 -1
  3. package/build/modules/errors.js +29 -1
  4. package/build/modules/key.d.ts +2 -2
  5. package/build/modules/key.js +3 -3
  6. package/build/package.json +2 -1
  7. package/build/services/activities/activity.d.ts +1 -0
  8. package/build/services/activities/activity.js +13 -2
  9. package/build/services/activities/cycle.js +6 -1
  10. package/build/services/activities/trigger.js +2 -3
  11. package/build/services/collator/index.d.ts +8 -0
  12. package/build/services/collator/index.js +11 -1
  13. package/build/services/durable/factory.d.ts +18 -1
  14. package/build/services/durable/factory.js +46 -4
  15. package/build/services/durable/handle.js +25 -7
  16. package/build/services/durable/native.js +0 -1
  17. package/build/services/durable/worker.d.ts +3 -3
  18. package/build/services/durable/worker.js +16 -11
  19. package/build/services/durable/workflow.js +1 -1
  20. package/build/services/hotmesh/index.js +1 -1
  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/services/store/index.js +2 -2
  27. package/build/services/stream/clients/ioredis.js +1 -1
  28. package/build/services/stream/clients/redis.js +1 -1
  29. package/build/services/sub/clients/ioredis.js +1 -1
  30. package/build/services/sub/clients/redis.js +1 -1
  31. package/build/types/durable.d.ts +8 -3
  32. package/build/types/index.d.ts +1 -0
  33. package/modules/errors.ts +42 -1
  34. package/modules/key.ts +2 -2
  35. package/package.json +2 -1
  36. package/services/activities/activity.ts +14 -2
  37. package/services/activities/cycle.ts +6 -1
  38. package/services/activities/trigger.ts +2 -3
  39. package/services/collator/index.ts +12 -1
  40. package/services/durable/factory.ts +46 -4
  41. package/services/durable/handle.ts +23 -8
  42. package/services/durable/native.ts +0 -1
  43. package/services/durable/worker.ts +27 -13
  44. package/services/durable/workflow.ts +1 -1
  45. package/services/hotmesh/index.ts +2 -2
  46. package/services/pipe/functions/math.ts +74 -0
  47. package/services/pipe/functions/number.ts +0 -75
  48. package/services/signaler/stream.ts +6 -3
  49. package/services/store/index.ts +3 -3
  50. package/services/stream/clients/ioredis.ts +2 -2
  51. package/services/stream/clients/redis.ts +2 -2
  52. package/services/sub/clients/ioredis.ts +2 -2
  53. package/services/sub/clients/redis.ts +2 -2
  54. package/types/durable.ts +12 -5
  55. package/types/index.ts +15 -0
  56. package/build/services/dimension/index.d.ts +0 -29
  57. package/build/services/dimension/index.js +0 -35
  58. package/services/dimension/README.md +0 -73
  59. package/services/dimension/index.ts +0 -39
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # HotMesh
2
2
  ![alpha release](https://img.shields.io/badge/release-alpha-yellow)
3
3
 
4
- Elevate Redis from an in-memory data store to a game-changing **service mesh**, delivering *durable* workflows without the overhead of a dedicated control plane. With HotMesh, you can keep your code at the forefront, utilizing [Redis infrastructure](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/faq.md#what-is-hotmesh) you already trust and own.
4
+ Elevate Redis from an in-memory data store to a game-changing [service mesh](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/faq.md#what-is-hotmesh). Turn your unpredictable functions into unbreakable workflows.
5
5
 
6
6
  ## Install
7
7
  [![npm version](https://badge.fury.io/js/%40hotmeshio%2Fhotmesh.svg)](https://badge.fury.io/js/%40hotmeshio%2Fhotmesh)
@@ -11,85 +11,92 @@ npm install @hotmeshio/hotmesh
11
11
  ```
12
12
 
13
13
  ## Design
14
- HotMesh's TypeScript SDK is modeled after Temporal IO's developer-friendly approach. Design and deploy durable workflows using your preferred coding style. Write your functions as you normally would, then use the HotMesh to make them durable. Temporal's [hello-world tutorial](https://github.com/temporalio/samples-typescript/tree/main/hello-world/src), for example, requires few changes beyond importing the HotMesh SDK.
14
+ The HotMesh SDK is designed to keep your code front-and-center. Write functions as you normally would, then use HotMesh to make them durable.
15
15
 
16
- >Start by defining activities. These are the functions that will be invoked by your workflow. They can be written in any style, using any framework, and can even be legacy functions you've already written. The only requirement is that they return a Promise.
16
+ 1. Start by defining **activities**. Activities are those functions that will be invoked by your workflow. They are commonly used to read and write to databases and invoke external services. They can be written in any style, using any framework, and can even be legacy functions you've already written. The only requirement is that they return a Promise. *Note how the `saludar` example throws an error 50% of the time. It doesn't matter how unpredictable your functions are, HotMesh will retry as necessary until they succeed.*
17
+ ```javascript
18
+ //activities.ts
17
19
 
18
- **./activities.ts**
19
- ```javascript
20
- export async function greet(name: string): Promise<string> {
21
- return `Hello, ${name}!`;
22
- }
23
- ```
24
-
25
- >Next, define your workflow. Include conditional logic, loops, etc. It's vanilla code written in your own coding style--just make sure to call `proxyActivities` to run your activities durably.
26
-
27
- **./workflows.ts**
28
- ```javascript
29
- import { Durable } from '@hotmeshio/hotmesh';
30
- import * as activities from './activities';
31
-
32
- const { greet } = Durable.workflow
33
- .proxyActivities<typeof activities>({
34
- activities
35
- });
36
-
37
- export async function example(name: string): Promise<string> {
38
- return await greet(name);
39
- }
40
- ```
41
-
42
- >Finally, create a worker and client. The *client* triggers workflows, while the *worker* runs them, retrying as necessary until the workflow succeeds--all without the need for complicated retry logic.
43
-
44
- **./client.ts**
45
- ```javascript
46
- import { Durable } from '@hotmeshio/hotmesh';
47
- import Redis from 'ioredis'; //OR `import * as Redis from 'redis';`
48
- import { v4 as uuidv4 } from 'uuid';
49
-
50
- async function run() {
51
- const connection = await Durable.Connection.connect({
52
- class: Redis,
53
- options: { host: 'localhost', port: 6379 }
54
- });
55
-
56
- const client = new Durable.Client({
57
- connection
58
- });
59
-
60
- const handle = await client.workflow.start({
61
- args: ['HotMesh'],
62
- taskQueue: 'hello-world',
63
- workflowName: 'example',
64
- workflowId: 'workflow-' + uuidv4()
65
- });
66
-
67
- console.log(await handle.result());
68
- }
69
- ```
20
+ export async function greet(name: string): Promise<string> {
21
+ return `Hello, ${name}!`;
22
+ }
70
23
 
71
- **./worker.ts**
72
- ```javascript
73
- import { Durable } from '@hotmeshio/hotmesh';
74
- import Redis from 'ioredis';
75
- import * as workflows from './workflows';
76
-
77
- async function run() {
78
- const connection = await Durable.NativeConnection.connect({
79
- class: Redis,
80
- options: { host: 'localhost', port: 6379 },
81
- });
82
- const worker = await Durable.Worker.create({
83
- connection,
84
- namespace: 'default',
85
- taskQueue: 'hello-world',
86
- workflow: workflows.example,
87
- });
88
- await worker.run();
89
- }
90
- ```
24
+ export async function saludar(nombre: string): Promise<string> {
25
+ Math.random() > 0.5 && throw new Error('Random error');
26
+ return `¡Hola, ${nombre}!`;
27
+ }
28
+ ```
29
+ 2. Define your **workflow** logic. Include conditional branching, loops, etc to control activity execution. It's vanilla code written in your own coding style. The only requirement is to use `proxyActivities`, ensuring your activities are executed with HotMesh's durability guarantee.
30
+ ```javascript
31
+ //workflows.ts
32
+
33
+ import { Durable } from '@hotmeshio/hotmesh';
34
+ import * as activities from './activities';
35
+
36
+ const { greet, saludar } = Durable.workflow
37
+ .proxyActivities<typeof activities>({
38
+ activities
39
+ });
40
+
41
+ export async function example(name: string, lang: string): Promise<string> {
42
+ if (lang === 'es') {
43
+ return await saludar(name);
44
+ } else {
45
+ return await greet(name);
46
+ }
47
+ }
48
+ ```
49
+
50
+ 3. Although you could call your workflow directly (it's just a vanilla function), it's only durable when invoked and orchestrated via HotMesh. By using a HotMesh **client** to send the request, the function is guaranteed to return a result.
51
+ ```javascript
52
+ //client.ts
53
+
54
+ import { Durable } from '@hotmeshio/hotmesh';
55
+ import Redis from 'ioredis'; //OR `import * as Redis from 'redis';`
56
+ import { v4 as uuidv4 } from 'uuid';
57
+
58
+ async function run(): Promise<string> {
59
+ const client = new Durable.Client({
60
+ connection: {
61
+ class: Redis,
62
+ options: { host: 'localhost', port: 6379 }
63
+ }
64
+ });
65
+
66
+ const handle = await client.workflow.start({
67
+ args: ['HotMesh', 'es'],
68
+ taskQueue: 'hello-world',
69
+ workflowName: 'example',
70
+ workflowId: uuidv4()
71
+ });
72
+
73
+ return await handle.result();
74
+ //returns '¡Hola, HotMesh!'
75
+ }
76
+ ```
77
+
78
+ 4. The last step is to create a **worker** and link it to your workflow function. Workers listen for tasks on their assigned channel, executing the linked workflow function until it succeeds.
79
+ ```javascript
80
+ //worker.ts
81
+
82
+ import { Durable } from '@hotmeshio/hotmesh';
83
+ import Redis from 'ioredis';
84
+ import * as workflows from './workflows';
85
+
86
+ async function run() {
87
+ const worker = await Durable.Worker.create({
88
+ connection: {
89
+ class: Redis,
90
+ options: { host: 'localhost', port: 6379 },
91
+ },
92
+ taskQueue: 'hello-world',
93
+ workflow: workflows.example,
94
+ });
95
+ await worker.run();
96
+ }
97
+ ```
91
98
 
92
- >HotMesh delivers durable function execution using a [distributed service mesh](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/distributed_orchestration.md). The design delivers durable workflows without the cost and complexity of a centralized service mesh/control plane. Refer to the [hotmeshio/samples-typescript](https://github.com/hotmeshio/samples-typescript) Git Repo for a range of examples, including nested workflows.
99
+ >HotMesh delivers durable workflows without the cost and complexity of a centralized service mesh. Refer to the [samples-typescript](https://github.com/hotmeshio/samples-typescript) Git Repo for a range of examples, including compositional workflows (where one workflow calls another) and remote execution (where calls are brokered across microservices).
93
100
 
94
101
  ## Advanced Design
95
102
  HotMesh's TypeScript SDK is the easiest way to make your functions durable. But if you need full control over your function lifecycles (including high-volume, high-speed use cases), you can use HotMesh's underlying YAML models to optimize your durable workflows. The following model depicts a sequence of activities orchestrated by HotMesh. Any function you associate with a `topic` in your YAML definition is guaranteed to be durable.
@@ -226,7 +233,7 @@ For a deep dive into HotMesh's distributed orchestration philosophy, refer to th
226
233
  HotMesh is a distributed orchestration engine. Refer to the [Distributed Orchestration Guide](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/distributed_orchestration.md) for a detailed breakdown of the approach.
227
234
 
228
235
  ## System Lifecycle
229
- Gain insight into the HotMesh's monitoring, exception handling, and alarm configurations via the [System Lifecycle Guide](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/system_lifecycle.md).
236
+ Gain insight into HotMesh's monitoring, exception handling, and alarm configurations via the [System Lifecycle Guide](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/system_lifecycle.md).
230
237
 
231
238
  ## Alpha Release
232
239
  So what exacty is an [alpha release](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/alpha.md)?!
@@ -6,6 +6,22 @@ declare class GetStateError extends Error {
6
6
  declare class SetStateError extends Error {
7
7
  constructor();
8
8
  }
9
+ declare class DurableTimeoutError extends Error {
10
+ code: number;
11
+ constructor(message: string);
12
+ }
13
+ declare class DurableMaxedError extends Error {
14
+ code: number;
15
+ constructor(message: string);
16
+ }
17
+ declare class DurableFatalError extends Error {
18
+ code: number;
19
+ constructor(message: string);
20
+ }
21
+ declare class DurableRetryError extends Error {
22
+ code: number;
23
+ constructor(message: string);
24
+ }
9
25
  declare class MapDataError extends Error {
10
26
  constructor();
11
27
  }
@@ -25,4 +41,4 @@ declare class CollationError extends Error {
25
41
  fault: CollationFaultType;
26
42
  constructor(status: number, leg: ActivityDuplex, stage: CollationStage, fault?: CollationFaultType);
27
43
  }
28
- export { CollationError, DuplicateJobError, GetStateError, SetStateError, MapDataError, RegisterTimeoutError, ExecActivityError };
44
+ export { CollationError, DurableTimeoutError, DurableMaxedError, DurableFatalError, DurableRetryError, DuplicateJobError, GetStateError, SetStateError, MapDataError, RegisterTimeoutError, ExecActivityError };
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ExecActivityError = exports.RegisterTimeoutError = exports.MapDataError = exports.SetStateError = exports.GetStateError = exports.DuplicateJobError = exports.CollationError = void 0;
3
+ exports.ExecActivityError = exports.RegisterTimeoutError = exports.MapDataError = exports.SetStateError = exports.GetStateError = exports.DuplicateJobError = exports.DurableRetryError = exports.DurableFatalError = exports.DurableMaxedError = exports.DurableTimeoutError = exports.CollationError = void 0;
4
4
  class GetStateError extends Error {
5
5
  constructor() {
6
6
  super("Error occurred while getting job state");
@@ -13,6 +13,34 @@ class SetStateError extends Error {
13
13
  }
14
14
  }
15
15
  exports.SetStateError = SetStateError;
16
+ class DurableTimeoutError extends Error {
17
+ constructor(message) {
18
+ super(message);
19
+ this.code = 596;
20
+ }
21
+ }
22
+ exports.DurableTimeoutError = DurableTimeoutError;
23
+ class DurableMaxedError extends Error {
24
+ constructor(message) {
25
+ super(message);
26
+ this.code = 597;
27
+ }
28
+ }
29
+ exports.DurableMaxedError = DurableMaxedError;
30
+ class DurableFatalError extends Error {
31
+ constructor(message) {
32
+ super(message);
33
+ this.code = 598;
34
+ }
35
+ }
36
+ exports.DurableFatalError = DurableFatalError;
37
+ class DurableRetryError extends Error {
38
+ constructor(message) {
39
+ super(message);
40
+ this.code = 599;
41
+ }
42
+ }
43
+ exports.DurableRetryError = DurableRetryError;
16
44
  class MapDataError extends Error {
17
45
  constructor() {
18
46
  super("Error occurred while mapping data");
@@ -25,7 +25,7 @@
25
25
  * hmsh:<appid>:sym:keys:<activityid|$subscribes> -> {hash} list of symbols based upon schema enums (initially) and adaptively optimized (later) during runtime; if '$subscribes' is used as the activityid, it is a top-level `job` symbol set (for job keys)
26
26
  * hmsh:<appid>:sym:vals: -> {hash} list of symbols for job values across all app versions
27
27
  */
28
- declare const PSNS = "hmsh";
28
+ declare const HMNS = "hmsh";
29
29
  declare enum KeyType {
30
30
  APP = 0,
31
31
  ENGINE_ID = 1,
@@ -72,4 +72,4 @@ declare class KeyService {
72
72
  */
73
73
  static mintKey(namespace: string, keyType: KeyType, params: KeyStoreParams): string;
74
74
  }
75
- export { KeyService, KeyType, KeyStoreParams, PSNS };
75
+ export { KeyService, KeyType, KeyStoreParams, HMNS };
@@ -27,10 +27,10 @@
27
27
  * hmsh:<appid>:sym:vals: -> {hash} list of symbols for job values across all app versions
28
28
  */
29
29
  Object.defineProperty(exports, "__esModule", { value: true });
30
- exports.PSNS = exports.KeyType = exports.KeyService = void 0;
30
+ exports.HMNS = exports.KeyType = exports.KeyService = void 0;
31
31
  //default namespace for hotmesh
32
- const PSNS = "hmsh";
33
- exports.PSNS = PSNS;
32
+ const HMNS = "hmsh";
33
+ exports.HMNS = HMNS;
34
34
  //these are the entity types that are stored in the key/value store
35
35
  var KeyType;
36
36
  (function (KeyType) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.9",
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
  },
@@ -40,6 +40,7 @@ declare class Activity {
40
40
  resolveStatus(multiResponse: MultiResponseFlags): number;
41
41
  mapJobData(): void;
42
42
  mapInputData(): void;
43
+ mapOutputData(): void;
43
44
  registerTimeout(): Promise<void>;
44
45
  bindActivityError(data: Record<string, unknown>): void;
45
46
  getTriggerConfig(): Promise<ActivityType>;
@@ -4,7 +4,6 @@ exports.Activity = void 0;
4
4
  const errors_1 = require("../../modules/errors");
5
5
  const utils_1 = require("../../modules/utils");
6
6
  const collator_1 = require("../collator");
7
- const dimension_1 = require("../dimension");
8
7
  const mapper_1 = require("../mapper");
9
8
  const pipe_1 = require("../pipe");
10
9
  const serializer_1 = require("../serializer");
@@ -43,6 +42,7 @@ class Activity {
43
42
  if (this.doesHook()) {
44
43
  //sleep and wait to awaken upon a signal
45
44
  await this.registerHook(multi);
45
+ this.mapOutputData();
46
46
  this.mapJobData();
47
47
  await this.setState(multi);
48
48
  await collator_1.CollatorService.authorizeReentry(this, multi);
@@ -53,6 +53,7 @@ class Activity {
53
53
  else {
54
54
  //end the activity and transition to its children
55
55
  this.adjacencyList = await this.filterAdjacent();
56
+ this.mapOutputData();
56
57
  this.mapJobData();
57
58
  await this.setState(multi);
58
59
  await collator_1.CollatorService.notarizeEarlyCompletion(this, multi);
@@ -265,6 +266,16 @@ class Activity {
265
266
  this.context.data = mapper.mapRules();
266
267
  }
267
268
  }
269
+ mapOutputData() {
270
+ //activity YAML may include output map data that produces/extends activity output data.
271
+ if (this.config.output?.maps) {
272
+ const mapper = new mapper_1.MapperService(this.config.output.maps, this.context);
273
+ const actOutData = mapper.mapRules();
274
+ const activityId = this.metadata.aid;
275
+ const data = { ...this.context[activityId].output, ...actOutData };
276
+ this.context[activityId].output.data = data;
277
+ }
278
+ }
268
279
  async registerTimeout() {
269
280
  //set timeout in support of hook and/or duplex
270
281
  }
@@ -442,7 +453,7 @@ class Activity {
442
453
  }
443
454
  resolveAdjacentDad() {
444
455
  //concat self and child dimension (all children (leg 1) begin life at 0)
445
- return `${this.resolveDad()}${dimension_1.DimensionService.getSeed(0)}`;
456
+ return `${this.resolveDad()}${collator_1.CollatorService.getDimensionalSeed(0)}`;
446
457
  }
447
458
  ;
448
459
  async filterAdjacent() {
@@ -63,13 +63,18 @@ class Cycle extends activity_1.Activity {
63
63
  * pattern allows for retries without violating the DAG.
64
64
  */
65
65
  async cycleAncestorActivity(multi) {
66
+ //Cycle activity L1 is a standin for the target ancestor L1.
67
+ //Input data mapping (mapInputData) allows for the
68
+ //next dimensonal thread to execute with different
69
+ //input data than the current dimensional thread
70
+ this.mapInputData();
66
71
  const streamData = {
67
72
  metadata: {
68
73
  dad: collator_1.CollatorService.resolveReentryDimension(this),
69
74
  jid: this.context.metadata.jid,
70
75
  aid: this.config.ancestor,
71
76
  },
72
- data: {} //todo: verify immutability, before enabling: `this.context.data`
77
+ data: this.context.data
73
78
  };
74
79
  return (await this.engine.streamSignaler?.publishMessage(null, streamData, multi));
75
80
  }
@@ -6,7 +6,6 @@ const errors_1 = require("../../modules/errors");
6
6
  const utils_1 = require("../../modules/utils");
7
7
  const activity_1 = require("./activity");
8
8
  const collator_1 = require("../collator");
9
- const dimension_1 = require("../dimension");
10
9
  const pipe_1 = require("../pipe");
11
10
  const reporter_1 = require("../reporter");
12
11
  const serializer_1 = require("../serializer");
@@ -80,7 +79,7 @@ class Trigger extends activity_1.Activity {
80
79
  const jobKey = this.resolveJobKey(inputContext);
81
80
  const utc = (0, utils_1.formatISODate)(new Date());
82
81
  const { id, version } = await this.engine.getVID();
83
- this.initDimensionalAddress(dimension_1.DimensionService.getSeed());
82
+ this.initDimensionalAddress(collator_1.CollatorService.getDimensionalSeed());
84
83
  const activityMetadata = {
85
84
  ...this.metadata,
86
85
  jid: jobId,
@@ -100,7 +99,7 @@ class Trigger extends activity_1.Activity {
100
99
  trc: this.context.metadata.trc,
101
100
  spn: this.context.metadata.spn,
102
101
  jid: jobId,
103
- dad: dimension_1.DimensionService.getSeed(),
102
+ dad: collator_1.CollatorService.getDimensionalSeed(),
104
103
  key: jobKey,
105
104
  jc: utc,
106
105
  ju: utc,
@@ -69,5 +69,13 @@ declare class CollatorService {
69
69
  */
70
70
  static bindAncestorArray(graphs: HotMeshGraph[]): void;
71
71
  static isActivityComplete(status: number): boolean;
72
+ /**
73
+ * All activities exist on a dimensional plane. Zero
74
+ * is the default. A value of
75
+ * `AxY,0,0,0,0,1,0,0` would reflect that
76
+ * an ancestor activity was dimensionalized beyond
77
+ * the default.
78
+ */
79
+ static getDimensionalSeed(index?: number): string;
72
80
  }
73
81
  export { CollatorService };
@@ -75,7 +75,7 @@ class CollatorService {
75
75
  static async notarizeCompletion(activity, multi) {
76
76
  //1) ALWAYS actualize leg2 dimension (+1)
77
77
  //2) IF the activity is used in a cycle, don't close leg 2!
78
- const decrement = activity.config.cycle ? 0 : -1000000000000;
78
+ const decrement = activity.config.cycle ? 0 : 1000000000000;
79
79
  return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1 - decrement, this.getDimensionalAddress(activity), multi);
80
80
  }
81
81
  ;
@@ -218,6 +218,16 @@ class CollatorService {
218
218
  static isActivityComplete(status) {
219
219
  return (status - 0) <= 0;
220
220
  }
221
+ /**
222
+ * All activities exist on a dimensional plane. Zero
223
+ * is the default. A value of
224
+ * `AxY,0,0,0,0,1,0,0` would reflect that
225
+ * an ancestor activity was dimensionalized beyond
226
+ * the default.
227
+ */
228
+ static getDimensionalSeed(index = 0) {
229
+ return `,${index}`;
230
+ }
221
231
  }
222
232
  exports.CollatorService = CollatorService;
223
233
  //max int digit count that supports `hincrby`
@@ -1,3 +1,20 @@
1
- declare const getWorkflowYAML: (topic: string, version?: string) => string;
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
+ declare const getWorkflowYAML: (topic: string, version?: string, maxSystemRetries?: number, backoffExponent?: number) => string;
2
19
  declare const getActivityYAML: (topic: string, version?: string) => string;
3
20
  export { getActivityYAML, getWorkflowYAML };
@@ -1,7 +1,24 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getWorkflowYAML = exports.getActivityYAML = void 0;
4
- const getWorkflowYAML = (topic, version = '1') => {
4
+ /**
5
+ * 1) `maxSystemRetries` | can be 0 to 3 and represents milliseconds;
6
+ * if there is an error, the workflow will retry up to `maxSystemRetries` times
7
+ * delaying by 10, 100, and 1000ms; this is a system level retry
8
+ * and is not configurable. It exists to handle intermittent network
9
+ * errors. (NOTE: each retry spawns a new transition stream)
10
+ *
11
+ * 2) `backoffExponent` | can be any number and represents `seconds` when applied;
12
+ * retries will happen indefinitely and adhere to the
13
+ * exponential backoff algorithm by multiplying by `backoffExponent`.
14
+ * For example, if `backoffExponent` is 10, the workflow will retry
15
+ * in 10s, 100s, 1000s, 10000s, etc.
16
+ *
17
+ * EXAMPLE | Using `maxSystemRetries = 3` and `backoffExponent = 10`, errant
18
+ * workflows will be retried on the following schedule (8 times in 27 hours):
19
+ * => 10ms, 100ms, 1000ms, 10s, 100s, 1_000s, 10_000s, 100_000s
20
+ */
21
+ const getWorkflowYAML = (topic, version = '1', maxSystemRetries = 2, backoffExponent = 10) => {
5
22
  return `app:
6
23
  id: ${topic}
7
24
  version: '${version}'
@@ -33,10 +50,20 @@ const getWorkflowYAML = (topic, version = '1') => {
33
50
  a1:
34
51
  type: activity
35
52
  cycle: true
36
-
53
+ output:
54
+ schema:
55
+ type: object
56
+ properties:
57
+ duration:
58
+ type: number
59
+ maps:
60
+ duration: ${backoffExponent}
61
+
37
62
  w1:
38
63
  type: worker
39
64
  topic: ${topic}
65
+ retry:
66
+ '599': [${maxSystemRetries}]
40
67
  input:
41
68
  schema:
42
69
  type: object
@@ -58,18 +85,33 @@ const getWorkflowYAML = (topic, version = '1') => {
58
85
  maps:
59
86
  response: '{$self.output.data.response}'
60
87
 
88
+ a599:
89
+ title: Sleep before trying again
90
+ type: activity
91
+ sleep: "{a1.output.data.duration}"
92
+
61
93
  c1:
94
+ title: Goto Activity a1
62
95
  type: cycle
63
96
  ancestor: a1
97
+ input:
98
+ maps:
99
+ duration:
100
+ '@pipe':
101
+ - ['{a1.output.data.duration}', ${backoffExponent}]
102
+ - ['{@math.multiply}']
103
+
64
104
  transitions:
65
105
  t1:
66
106
  - to: a1
67
107
  a1:
68
108
  - to: w1
69
109
  w1:
70
- - to: c1
110
+ - to: a599
71
111
  conditions:
72
- code: 500
112
+ code: 599
113
+ a599:
114
+ - to: c1
73
115
  `;
74
116
  };
75
117
  exports.getWorkflowYAML = getWorkflowYAML;
@@ -10,22 +10,40 @@ class WorkflowHandleService {
10
10
  async result() {
11
11
  let status = await this.hotMesh.getStatus(this.workflowId);
12
12
  const topic = `${this.workflowTopic}.${this.workflowId}`;
13
- if (status == 0) {
14
- return (await this.hotMesh.getState(this.workflowTopic, this.workflowId)).data?.response;
15
- }
16
13
  return new Promise((resolve, reject) => {
17
14
  let isResolved = false;
18
15
  //common fulfill/unsubscribe
19
- const complete = async (response) => {
16
+ const complete = async (response, err) => {
20
17
  if (isResolved)
21
18
  return;
22
19
  isResolved = true;
23
20
  this.hotMesh.unsub(topic);
24
- resolve(response || (await this.hotMesh.getState(this.workflowTopic, this.workflowId)).data?.response);
21
+ if (err) {
22
+ return reject(JSON.parse(err));
23
+ }
24
+ else if (!response) {
25
+ const state = await this.hotMesh.getState(this.workflowTopic, this.workflowId);
26
+ if (!state.data && state.metadata.err) {
27
+ return reject(JSON.parse(state.metadata.err));
28
+ }
29
+ response = state.data?.response;
30
+ }
31
+ resolve(response);
25
32
  };
26
- this.hotMesh.sub(topic, async (topic, message) => {
27
- await complete(message.data?.response);
33
+ //check for done
34
+ if (status == 0) {
35
+ return complete();
36
+ }
37
+ //subscribe to topic
38
+ this.hotMesh.sub(topic, async (topic, state) => {
39
+ if (!state.data && state.metadata.err) {
40
+ await complete(null, state.metadata.err);
41
+ }
42
+ else {
43
+ await complete(state.data?.response);
44
+ }
28
45
  });
46
+ //resolve for race condition
29
47
  setTimeout(async () => {
30
48
  status = await this.hotMesh.getStatus(this.workflowId);
31
49
  if (status == 0) {
@@ -22,7 +22,6 @@ async function run() {
22
22
  });
23
23
  const worker = await Worker.create({
24
24
  connection,
25
- namespace: 'default',
26
25
  taskQueue: 'hello-world',
27
26
  workflow: workflows.example,
28
27
  activities,
@@ -1,13 +1,13 @@
1
1
  import { HotMeshService as HotMesh } from '../hotmesh';
2
- import { Connection, Registry, WorkerConfig } from "../../types/durable";
2
+ import { Connection, Registry, WorkerConfig, WorkerOptions } from "../../types/durable";
3
3
  export declare class WorkerService {
4
4
  static activityRegistry: Registry;
5
5
  static connection: Connection;
6
6
  static instances: Map<string, HotMesh | Promise<HotMesh>>;
7
7
  workflowRunner: HotMesh;
8
8
  activityRunner: HotMesh;
9
- static getHotMesh: (worflowTopic: string) => Promise<HotMesh>;
10
- static activateWorkflow(hotMesh: HotMesh, topic: string, factory: Function): Promise<void>;
9
+ static getHotMesh: (worflowTopic: string, options?: WorkerOptions) => Promise<HotMesh>;
10
+ static activateWorkflow(hotMesh: HotMesh, topic: string, dagFactory: Function, options?: WorkerOptions): Promise<void>;
11
11
  /**
12
12
  * The `worker` calls `registerActivities` immediately BEFORE
13
13
  * dynamically importing the user's workflow module. That file