@hotmeshio/hotmesh 0.0.34 → 0.0.35

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 (91) hide show
  1. package/README.md +30 -18
  2. package/build/modules/enums.d.ts +22 -0
  3. package/build/modules/enums.js +29 -0
  4. package/build/modules/errors.d.ts +10 -2
  5. package/build/modules/errors.js +14 -3
  6. package/build/modules/key.d.ts +16 -15
  7. package/build/modules/key.js +18 -15
  8. package/build/modules/utils.d.ts +1 -0
  9. package/build/modules/utils.js +6 -1
  10. package/build/package.json +3 -1
  11. package/build/services/activities/activity.d.ts +5 -0
  12. package/build/services/activities/activity.js +27 -6
  13. package/build/services/activities/await.js +11 -3
  14. package/build/services/activities/cycle.js +10 -2
  15. package/build/services/activities/hook.js +8 -2
  16. package/build/services/activities/index.d.ts +2 -2
  17. package/build/services/activities/index.js +2 -2
  18. package/build/services/activities/interrupt.d.ts +16 -0
  19. package/build/services/activities/interrupt.js +129 -0
  20. package/build/services/activities/signal.js +9 -2
  21. package/build/services/activities/trigger.d.ts +4 -0
  22. package/build/services/activities/trigger.js +14 -4
  23. package/build/services/activities/worker.js +10 -2
  24. package/build/services/collator/index.d.ts +4 -0
  25. package/build/services/collator/index.js +8 -0
  26. package/build/services/compiler/deployer.js +1 -3
  27. package/build/services/connector/index.js +2 -3
  28. package/build/services/durable/client.js +7 -3
  29. package/build/services/durable/factory.js +65 -284
  30. package/build/services/durable/handle.d.ts +37 -0
  31. package/build/services/durable/handle.js +52 -9
  32. package/build/services/durable/meshos.js +2 -2
  33. package/build/services/durable/worker.js +9 -2
  34. package/build/services/durable/workflow.d.ts +24 -0
  35. package/build/services/durable/workflow.js +56 -1
  36. package/build/services/engine/index.d.ts +14 -6
  37. package/build/services/engine/index.js +52 -27
  38. package/build/services/hotmesh/index.d.ts +3 -1
  39. package/build/services/hotmesh/index.js +11 -3
  40. package/build/services/quorum/index.d.ts +1 -0
  41. package/build/services/quorum/index.js +10 -0
  42. package/build/services/signaler/stream.js +25 -29
  43. package/build/services/store/index.d.ts +40 -4
  44. package/build/services/store/index.js +114 -9
  45. package/build/services/task/index.d.ts +5 -4
  46. package/build/services/task/index.js +12 -14
  47. package/build/types/activity.d.ts +35 -5
  48. package/build/types/durable.d.ts +4 -0
  49. package/build/types/index.d.ts +1 -1
  50. package/build/types/job.d.ts +18 -1
  51. package/build/types/quorum.d.ts +11 -7
  52. package/build/types/stream.d.ts +4 -1
  53. package/build/types/stream.js +2 -0
  54. package/modules/enums.ts +32 -0
  55. package/modules/errors.ts +24 -9
  56. package/modules/key.ts +4 -1
  57. package/modules/utils.ts +5 -0
  58. package/package.json +3 -1
  59. package/services/activities/activity.ts +34 -8
  60. package/services/activities/await.ts +11 -4
  61. package/services/activities/cycle.ts +10 -3
  62. package/services/activities/hook.ts +8 -3
  63. package/services/activities/index.ts +2 -2
  64. package/services/activities/interrupt.ts +159 -0
  65. package/services/activities/signal.ts +9 -3
  66. package/services/activities/trigger.ts +21 -5
  67. package/services/activities/worker.ts +10 -3
  68. package/services/collator/index.ts +10 -1
  69. package/services/compiler/deployer.ts +1 -3
  70. package/services/connector/index.ts +3 -5
  71. package/services/durable/client.ts +8 -4
  72. package/services/durable/factory.ts +65 -284
  73. package/services/durable/handle.ts +55 -9
  74. package/services/durable/meshos.ts +2 -3
  75. package/services/durable/worker.ts +9 -2
  76. package/services/durable/workflow.ts +66 -2
  77. package/services/engine/index.ts +74 -26
  78. package/services/hotmesh/index.ts +14 -4
  79. package/services/quorum/index.ts +9 -0
  80. package/services/signaler/stream.ts +27 -24
  81. package/services/store/index.ts +119 -11
  82. package/services/task/index.ts +18 -18
  83. package/types/activity.ts +38 -8
  84. package/types/durable.ts +8 -4
  85. package/types/index.ts +1 -1
  86. package/types/job.ts +30 -1
  87. package/types/quorum.ts +13 -8
  88. package/types/stream.ts +3 -0
  89. package/build/services/activities/iterate.d.ts +0 -9
  90. package/build/services/activities/iterate.js +0 -13
  91. package/services/activities/iterate.ts +0 -26
package/README.md CHANGED
@@ -1,9 +1,9 @@
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 cache, and turn your unpredictable functions into unbreakable workflows. HotMesh is a distributed orchestration engine that governs the execution of your functions, ensuring they always complete successfully.
4
+ HotMesh elevates Redis from an in-memory data cache to a distributed orchestration engine.
5
5
 
6
- *Write functions in your own preferred style, and let Redis govern their execution.*
6
+ *Write functions in your own preferred style, and let Redis govern their execution, reliably and durably.*
7
7
 
8
8
  ## Install
9
9
  [![npm version](https://badge.fury.io/js/%40hotmeshio%2Fhotmesh.svg)](https://badge.fury.io/js/%40hotmeshio%2Fhotmesh)
@@ -11,7 +11,16 @@ Elevate Redis from an in-memory data cache, and turn your unpredictable function
11
11
  ```sh
12
12
  npm install @hotmeshio/hotmesh
13
13
  ```
14
+
15
+ ## Understanding HotMesh
16
+ HotMesh inverts the relationship to Redis: those functions that once used Redis as a cache, are instead *cached and governed* by Redis. Consider the following. It's a typical microservices network, with a tangled mess of services and functions. There's important business logic in there (functions *A*, *B* and *C* are critical!), but they're hard to find and access.
17
+
18
+ <img src="https://github.com/hotmeshio/sdk-typescript/blob/main/docs/img/operational_data_layer.png" alt="A Tangled Microservices Network with 3 functions buried within" style="max-width:100%;width:600px;">
19
+
20
+ HotMesh creates an *ad hoc*, Redis-backed network of functions and organizes them into a unified service mesh. *Any service with access to Redis can join in the network, bypassing the legacy clutter.*
21
+
14
22
  ## Design
23
+ The simplest way to get started is to use the `Durable` module. It's organized using principles similar to Temporal. If you're familiar with their SDK, the setup is similar.
15
24
 
16
25
  1. Start by defining **activities**. Activities 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
26
  ```javascript
@@ -47,9 +56,8 @@ npm install @hotmeshio/hotmesh
47
56
  3. Instance a HotMesh **client** to invoke the workflow.
48
57
  ```javascript
49
58
  //client.ts
50
- import { Durable } from '@hotmeshio/hotmesh';
59
+ import { Durable, HotMesh } from '@hotmeshio/hotmesh';
51
60
  import Redis from 'ioredis'; //OR `import * as Redis from 'redis';`
52
- import { nanoid } from 'nanoid';
53
61
 
54
62
  async function run(): Promise<string> {
55
63
  const client = new Durable.Client({
@@ -61,9 +69,9 @@ npm install @hotmeshio/hotmesh
61
69
 
62
70
  const handle = await client.workflow.start({
63
71
  args: ['HotMesh', 'es'],
64
- taskQueue: 'hello-world',
72
+ taskQueue: 'default',
65
73
  workflowName: 'example',
66
- workflowId: nanoid()
74
+ workflowId: HotMesh.guid()
67
75
  });
68
76
 
69
77
  return await handle.result();
@@ -83,7 +91,7 @@ npm install @hotmeshio/hotmesh
83
91
  class: Redis,
84
92
  options: { host: 'localhost', port: 6379 },
85
93
  },
86
- taskQueue: 'hello-world',
94
+ taskQueue: 'default',
87
95
  workflow: workflows.example,
88
96
  });
89
97
 
@@ -92,7 +100,7 @@ npm install @hotmeshio/hotmesh
92
100
  ```
93
101
 
94
102
  ### Workflow Extensions
95
- Redis governance delivers more than just reliability. Externalizing state fundamentally changes the execution profile for your functions, allowing you to design long-running, durable workflows. The `Durable.workflow` base class (shown in the examples above) provides additional methods for solving the most common state management challenges.
103
+ Redis governance delivers more than just reliability. Externalizing state fundamentally changes the execution profile for your functions, allowing you to design long-running, durable workflows. The `Durable` base class (shown in the examples above) provides additional methods for solving the most common state management challenges.
96
104
 
97
105
  - `waitForSignal` Pause your function and wait for external event(s) before continuing. The *waitForSignal* method will collate and cache the signals and only awaken your function once all signals have arrived.
98
106
  ```javascript
@@ -134,6 +142,10 @@ Redis governance delivers more than just reliability. Externalizing state fundam
134
142
  args: [{ id, user_id, etc }],
135
143
  });
136
144
  ```
145
+ - `getContext` Get the current workflow context (workflowId, etc).
146
+ ```javascript
147
+ const context = await Durable.workflow.getContext();
148
+ ```
137
149
  - `search` Instance a search session
138
150
  ```javascript
139
151
  const search = await Durable.workflow.search();
@@ -273,31 +285,31 @@ const hotMesh = await HotMesh.init({
273
285
  ```
274
286
 
275
287
  ### Observability
276
- Workflows and activities are run according to the rules you define, offering [Graph-Oriented](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/system_lifecycle.md#telemetry) telemetry insights into your legacy function executions.
288
+ Workflows and activities are run according to the rules you define, offering [Graph-Oriented](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/system_lifecycle.md#telemetry) telemetry insights into your legacy function executions.
277
289
 
278
290
  ## FAQ
279
- Refer to the [FAQ](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/faq.md) for terminology, definitions, and an exploration of how HotMesh facilitates orchestration use cases.
291
+ Refer to the [FAQ](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/faq.md) for terminology, definitions, and an exploration of how HotMesh facilitates orchestration use cases.
280
292
 
281
293
  ## Quick Start
282
- Refer to the [Quick Start](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/quickstart.md) for sample flows you can easily copy, paste, and modify to get started.
294
+ Refer to the [Quick Start](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/quickstart.md) for sample flows you can easily copy, paste, and modify to get started.
283
295
 
284
296
  ## Developer Guide
285
- For more details on the complete development process, including information about schemas, APIs, and deployment, consult the [Developer Guide](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/developer_guide.md).
297
+ For more details on the complete development process, including information about schemas, APIs, and deployment, consult the [Developer Guide](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/developer_guide.md).
286
298
 
287
299
  ## Model Driven Development
288
- [Model Driven Development](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/model_driven_development.md) is an established strategy for managing process-oriented tasks. Check out this guide to understand its foundational principles.
300
+ [Model Driven Development](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/model_driven_development.md) is an established strategy for managing process-oriented tasks. Check out this guide to understand its foundational principles.
289
301
 
290
302
  ## Data Mapping
291
- Exchanging data between activities is central to HotMesh. For detailed information on supported functions and the functional mapping syntax (@pipes), see the [Data Mapping Overview](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/data_mapping.md).
303
+ Exchanging data between activities is central to HotMesh. For detailed information on supported functions and the functional mapping syntax (@pipes), see the [Data Mapping Overview](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/data_mapping.md).
292
304
 
293
305
  ## Composition
294
- While the simplest graphs are linear, detailing a consistent sequence of non-cyclical activities, graphs can be layered to represent intricate business scenarios. Some can even be designed to accommodate long-lasting workflows that span months. For more details, check out the [Composable Workflow Guide](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/composable_workflow.md).
306
+ While the simplest graphs are linear, detailing a consistent sequence of non-cyclical activities, graphs can be layered to represent intricate business scenarios. Some can even be designed to accommodate long-lasting workflows that span months. For more details, check out the [Composable Workflow Guide](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/composable_workflow.md).
295
307
 
296
308
  ## Distributed Orchestration
297
- HotMesh is a distributed orchestration engine. Refer to the [Distributed Orchestration Guide](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/distributed_orchestration.md) for a detailed breakdown of the approach.
309
+ 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.
298
310
 
299
311
  ## System Lifecycle
300
- Gain insight into HotMesh's monitoring, exception handling, and alarm configurations via the [System Lifecycle Guide](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/system_lifecycle.md).
312
+ 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).
301
313
 
302
314
  ## Alpha Release
303
- So what exacty is an [alpha release](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/alpha.md)?
315
+ So what exacty is an [alpha release](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/alpha.md)?
@@ -0,0 +1,22 @@
1
+ export declare const STATUS_CODE_SUCCESS = 200;
2
+ export declare const STATUS_CODE_PENDING = 202;
3
+ export declare const STATUS_CODE_TIMEOUT = 504;
4
+ export declare const STATUS_CODE_INTERRUPT = 410;
5
+ export declare const OTT_WAIT_TIME = 1000;
6
+ export declare const MAX_RETRIES = 3;
7
+ export declare const MAX_TIMEOUT_MS = 60000;
8
+ export declare const GRADUATED_INTERVAL_MS = 5000;
9
+ export declare const BLOCK_DURATION = 15000;
10
+ export declare const TEST_BLOCK_DURATION = 1000;
11
+ export declare const BLOCK_TIME_MS: number;
12
+ export declare const XCLAIM_DELAY_MS: number;
13
+ export declare const XCLAIM_COUNT = 3;
14
+ export declare const XPENDING_COUNT = 10;
15
+ export declare const STATUS_CODE_UNACKED = 999;
16
+ export declare const STATUS_CODE_UNKNOWN = 500;
17
+ export declare const STATUS_MESSAGE_UNKNOWN = "unknown";
18
+ export declare const EXPIRE_DURATION = 15;
19
+ export declare const BASE_FIDELITY_SECONDS = 15;
20
+ export declare const TEST_FIDELITY_SECONDS = 5;
21
+ export declare const FIDELITY_SECONDS: number;
22
+ export declare const DURABLE_EXPIRE_SECONDS = 1;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DURABLE_EXPIRE_SECONDS = exports.FIDELITY_SECONDS = exports.TEST_FIDELITY_SECONDS = exports.BASE_FIDELITY_SECONDS = exports.EXPIRE_DURATION = exports.STATUS_MESSAGE_UNKNOWN = exports.STATUS_CODE_UNKNOWN = exports.STATUS_CODE_UNACKED = exports.XPENDING_COUNT = exports.XCLAIM_COUNT = exports.XCLAIM_DELAY_MS = exports.BLOCK_TIME_MS = exports.TEST_BLOCK_DURATION = exports.BLOCK_DURATION = exports.GRADUATED_INTERVAL_MS = exports.MAX_TIMEOUT_MS = exports.MAX_RETRIES = exports.OTT_WAIT_TIME = exports.STATUS_CODE_INTERRUPT = exports.STATUS_CODE_TIMEOUT = exports.STATUS_CODE_PENDING = exports.STATUS_CODE_SUCCESS = void 0;
4
+ // Engine Constants
5
+ exports.STATUS_CODE_SUCCESS = 200;
6
+ exports.STATUS_CODE_PENDING = 202;
7
+ exports.STATUS_CODE_TIMEOUT = 504;
8
+ exports.STATUS_CODE_INTERRUPT = 410;
9
+ exports.OTT_WAIT_TIME = 1000;
10
+ // Stream Constants
11
+ exports.MAX_RETRIES = 3; //local retry; 10, 100, 1000ms
12
+ exports.MAX_TIMEOUT_MS = 60000;
13
+ exports.GRADUATED_INTERVAL_MS = 5000;
14
+ exports.BLOCK_DURATION = 15000; //Set to `15` so SIGINT/SIGTERM can interrupt; set to `0` to BLOCK indefinitely
15
+ exports.TEST_BLOCK_DURATION = 1000; //Set to `1000` so tests can interrupt quickly
16
+ exports.BLOCK_TIME_MS = process.env.NODE_ENV === 'test' ? exports.TEST_BLOCK_DURATION : exports.BLOCK_DURATION;
17
+ exports.XCLAIM_DELAY_MS = 1000 * 60; //max time a message can be unacked before it is claimed by another
18
+ exports.XCLAIM_COUNT = 3; //max number of times a message can be claimed by another before it is dead-lettered
19
+ exports.XPENDING_COUNT = 10;
20
+ exports.STATUS_CODE_UNACKED = 999;
21
+ exports.STATUS_CODE_UNKNOWN = 500;
22
+ exports.STATUS_MESSAGE_UNKNOWN = 'unknown';
23
+ // HotMesh Constants
24
+ exports.EXPIRE_DURATION = 15; // default expire in seconds; once job state semaphore reaches '0', this is applied to set Redis to expire the job HASH
25
+ exports.BASE_FIDELITY_SECONDS = 15; // granularity resolution window size
26
+ exports.TEST_FIDELITY_SECONDS = 5;
27
+ exports.FIDELITY_SECONDS = process.env.NODE_ENV === 'test' ? exports.TEST_FIDELITY_SECONDS : exports.BASE_FIDELITY_SECONDS;
28
+ // DURABLE CONSTANTS
29
+ exports.DURABLE_EXPIRE_SECONDS = 1;
@@ -1,7 +1,9 @@
1
1
  import { ActivityDuplex } from "../types/activity";
2
2
  import { CollationFaultType, CollationStage } from "../types/collator";
3
3
  declare class GetStateError extends Error {
4
- constructor();
4
+ jobId: string;
5
+ code: 404;
6
+ constructor(jobId: string);
5
7
  }
6
8
  declare class SetStateError extends Error {
7
9
  constructor();
@@ -60,6 +62,12 @@ declare class RegisterTimeoutError extends Error {
60
62
  declare class DuplicateJobError extends Error {
61
63
  constructor(jobId: string);
62
64
  }
65
+ declare class InactiveJobError extends Error {
66
+ jobId: string;
67
+ activityId: string;
68
+ status: number;
69
+ constructor(jobId: string, status: number, activityId: string);
70
+ }
63
71
  declare class ExecActivityError extends Error {
64
72
  constructor();
65
73
  }
@@ -70,4 +78,4 @@ declare class CollationError extends Error {
70
78
  fault: CollationFaultType;
71
79
  constructor(status: number, leg: ActivityDuplex, stage: CollationStage, fault?: CollationFaultType);
72
80
  }
73
- export { CollationError, DurableTimeoutError, DurableMaxedError, DurableFatalError, DurableRetryError, DurableWaitForSignalError, DurableIncompleteSignalError, DurableSleepError, DurableSleepForError, DuplicateJobError, GetStateError, SetStateError, MapDataError, RegisterTimeoutError, ExecActivityError };
81
+ export { CollationError, DurableFatalError, DurableIncompleteSignalError, DurableMaxedError, DurableRetryError, DurableSleepError, DurableSleepForError, DurableTimeoutError, DurableWaitForSignalError, DuplicateJobError, ExecActivityError, GetStateError, InactiveJobError, MapDataError, RegisterTimeoutError, SetStateError, };
@@ -1,9 +1,10 @@
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.DurableSleepForError = exports.DurableSleepError = exports.DurableIncompleteSignalError = exports.DurableWaitForSignalError = exports.DurableRetryError = exports.DurableFatalError = exports.DurableMaxedError = exports.DurableTimeoutError = exports.CollationError = void 0;
3
+ exports.SetStateError = exports.RegisterTimeoutError = exports.MapDataError = exports.InactiveJobError = exports.GetStateError = exports.ExecActivityError = exports.DuplicateJobError = exports.DurableWaitForSignalError = exports.DurableTimeoutError = exports.DurableSleepForError = exports.DurableSleepError = exports.DurableRetryError = exports.DurableMaxedError = exports.DurableIncompleteSignalError = exports.DurableFatalError = exports.CollationError = void 0;
4
4
  class GetStateError extends Error {
5
- constructor() {
6
- super("Error occurred while getting job state");
5
+ constructor(jobId) {
6
+ super(`${jobId} Not Found`);
7
+ this.jobId = jobId;
7
8
  }
8
9
  }
9
10
  exports.GetStateError = GetStateError;
@@ -99,6 +100,16 @@ class DuplicateJobError extends Error {
99
100
  }
100
101
  }
101
102
  exports.DuplicateJobError = DuplicateJobError;
103
+ class InactiveJobError extends Error {
104
+ constructor(jobId, status, activityId) {
105
+ super("Inactive job");
106
+ this.jobId = jobId;
107
+ this.activityId = activityId;
108
+ this.message = `Inactive job: ${jobId}`;
109
+ this.status = status;
110
+ }
111
+ }
112
+ exports.InactiveJobError = InactiveJobError;
102
113
  class ExecActivityError extends Error {
103
114
  constructor() {
104
115
  super("Error occurred while executing activity");
@@ -30,21 +30,22 @@ declare enum KeyType {
30
30
  APP = 0,
31
31
  ENGINE_ID = 1,
32
32
  HOOKS = 2,
33
- JOB_STATE = 3,
34
- JOB_STATS_GENERAL = 4,
35
- JOB_STATS_MEDIAN = 5,
36
- JOB_STATS_INDEX = 6,
37
- HOTMESH = 7,
38
- QUORUM = 8,
39
- SCHEMAS = 9,
40
- SIGNALS = 10,
41
- STREAMS = 11,
42
- SUBSCRIPTIONS = 12,
43
- SUBSCRIPTION_PATTERNS = 13,
44
- SYMKEYS = 14,
45
- SYMVALS = 15,
46
- TIME_RANGE = 16,
47
- WORK_ITEMS = 17
33
+ JOB_DEPENDENTS = 3,
34
+ JOB_STATE = 4,
35
+ JOB_STATS_GENERAL = 5,
36
+ JOB_STATS_MEDIAN = 6,
37
+ JOB_STATS_INDEX = 7,
38
+ HOTMESH = 8,
39
+ QUORUM = 9,
40
+ SCHEMAS = 10,
41
+ SIGNALS = 11,
42
+ STREAMS = 12,
43
+ SUBSCRIPTIONS = 13,
44
+ SUBSCRIPTION_PATTERNS = 14,
45
+ SYMKEYS = 15,
46
+ SYMVALS = 16,
47
+ TIME_RANGE = 17,
48
+ WORK_ITEMS = 18
48
49
  }
49
50
  type KeyStoreParams = {
50
51
  appId?: string;
@@ -37,21 +37,22 @@ var KeyType;
37
37
  KeyType[KeyType["APP"] = 0] = "APP";
38
38
  KeyType[KeyType["ENGINE_ID"] = 1] = "ENGINE_ID";
39
39
  KeyType[KeyType["HOOKS"] = 2] = "HOOKS";
40
- KeyType[KeyType["JOB_STATE"] = 3] = "JOB_STATE";
41
- KeyType[KeyType["JOB_STATS_GENERAL"] = 4] = "JOB_STATS_GENERAL";
42
- KeyType[KeyType["JOB_STATS_MEDIAN"] = 5] = "JOB_STATS_MEDIAN";
43
- KeyType[KeyType["JOB_STATS_INDEX"] = 6] = "JOB_STATS_INDEX";
44
- KeyType[KeyType["HOTMESH"] = 7] = "HOTMESH";
45
- KeyType[KeyType["QUORUM"] = 8] = "QUORUM";
46
- KeyType[KeyType["SCHEMAS"] = 9] = "SCHEMAS";
47
- KeyType[KeyType["SIGNALS"] = 10] = "SIGNALS";
48
- KeyType[KeyType["STREAMS"] = 11] = "STREAMS";
49
- KeyType[KeyType["SUBSCRIPTIONS"] = 12] = "SUBSCRIPTIONS";
50
- KeyType[KeyType["SUBSCRIPTION_PATTERNS"] = 13] = "SUBSCRIPTION_PATTERNS";
51
- KeyType[KeyType["SYMKEYS"] = 14] = "SYMKEYS";
52
- KeyType[KeyType["SYMVALS"] = 15] = "SYMVALS";
53
- KeyType[KeyType["TIME_RANGE"] = 16] = "TIME_RANGE";
54
- KeyType[KeyType["WORK_ITEMS"] = 17] = "WORK_ITEMS";
40
+ KeyType[KeyType["JOB_DEPENDENTS"] = 3] = "JOB_DEPENDENTS";
41
+ KeyType[KeyType["JOB_STATE"] = 4] = "JOB_STATE";
42
+ KeyType[KeyType["JOB_STATS_GENERAL"] = 5] = "JOB_STATS_GENERAL";
43
+ KeyType[KeyType["JOB_STATS_MEDIAN"] = 6] = "JOB_STATS_MEDIAN";
44
+ KeyType[KeyType["JOB_STATS_INDEX"] = 7] = "JOB_STATS_INDEX";
45
+ KeyType[KeyType["HOTMESH"] = 8] = "HOTMESH";
46
+ KeyType[KeyType["QUORUM"] = 9] = "QUORUM";
47
+ KeyType[KeyType["SCHEMAS"] = 10] = "SCHEMAS";
48
+ KeyType[KeyType["SIGNALS"] = 11] = "SIGNALS";
49
+ KeyType[KeyType["STREAMS"] = 12] = "STREAMS";
50
+ KeyType[KeyType["SUBSCRIPTIONS"] = 13] = "SUBSCRIPTIONS";
51
+ KeyType[KeyType["SUBSCRIPTION_PATTERNS"] = 14] = "SUBSCRIPTION_PATTERNS";
52
+ KeyType[KeyType["SYMKEYS"] = 15] = "SYMKEYS";
53
+ KeyType[KeyType["SYMVALS"] = 16] = "SYMVALS";
54
+ KeyType[KeyType["TIME_RANGE"] = 17] = "TIME_RANGE";
55
+ KeyType[KeyType["WORK_ITEMS"] = 18] = "WORK_ITEMS";
55
56
  })(KeyType || (KeyType = {}));
56
57
  exports.KeyType = KeyType;
57
58
  class KeyService {
@@ -82,6 +83,8 @@ class KeyService {
82
83
  return `${namespace}:${params.appId}:q:${params.engineId || ''}`;
83
84
  case KeyType.JOB_STATE:
84
85
  return `${namespace}:${params.appId}:j:${params.jobId}`;
86
+ case KeyType.JOB_DEPENDENTS:
87
+ return `${namespace}:${params.appId}:d:${params.jobId}`;
85
88
  case KeyType.JOB_STATS_GENERAL:
86
89
  return `${namespace}:${params.appId}:s:${params.jobKey}:${params.dateTime}`;
87
90
  case KeyType.JOB_STATS_MEDIAN:
@@ -5,6 +5,7 @@ import { RedisClient, RedisMulti } from "../types/redis";
5
5
  import { StringAnyType } from "../types/serializer";
6
6
  import { StreamCode, StreamStatus } from "../types/stream";
7
7
  export declare function sleepFor(ms: number): Promise<unknown>;
8
+ export declare function guid(): string;
8
9
  export declare function deterministicRandom(seed: number): number;
9
10
  export declare function identifyRedisType(redisInstance: any): 'redis' | 'ioredis' | null;
10
11
  export declare const polyfill: {
@@ -1,10 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.restoreHierarchy = exports.getValueByPath = exports.getIndexedHash = exports.getSymVal = exports.getSymKey = exports.formatISODate = exports.getTimeSeries = exports.getSubscriptionTopic = exports.findSubscriptionForTrigger = exports.findTopKey = exports.XSleepFor = exports.matchesStatus = exports.matchesStatusCode = exports.identifyRedisTypeFromClass = exports.polyfill = exports.identifyRedisType = exports.deterministicRandom = exports.sleepFor = void 0;
3
+ exports.restoreHierarchy = exports.getValueByPath = exports.getIndexedHash = exports.getSymVal = exports.getSymKey = exports.formatISODate = exports.getTimeSeries = exports.getSubscriptionTopic = exports.findSubscriptionForTrigger = exports.findTopKey = exports.XSleepFor = exports.matchesStatus = exports.matchesStatusCode = exports.identifyRedisTypeFromClass = exports.polyfill = exports.identifyRedisType = exports.deterministicRandom = exports.guid = exports.sleepFor = void 0;
4
+ const nanoid_1 = require("nanoid");
4
5
  async function sleepFor(ms) {
5
6
  return new Promise((resolve) => setTimeout(resolve, ms));
6
7
  }
7
8
  exports.sleepFor = sleepFor;
9
+ function guid() {
10
+ return (0, nanoid_1.nanoid)();
11
+ }
12
+ exports.guid = guid;
8
13
  function deterministicRandom(seed) {
9
14
  let x = Math.sin(seed) * 10000;
10
15
  return x - Math.floor(x);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.34",
3
+ "version": "0.0.35",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -30,6 +30,7 @@
30
30
  "test:emit": "NODE_ENV=test jest ./tests/functional/emit/index.test.ts --detectOpenHandles --forceExit --verbose",
31
31
  "test:hook": "NODE_ENV=test jest ./tests/functional/hook/index.test.ts --detectOpenHandles --forceExit --verbose",
32
32
  "test:signal": "NODE_ENV=test jest ./tests/functional/signal/index.test.ts --detectOpenHandles --forceExit --verbose",
33
+ "test:interrupt": "NODE_ENV=test jest ./tests/functional/interrupt/index.test.ts --detectOpenHandles --forceExit --verbose",
33
34
  "test:parallel": "NODE_ENV=test jest ./tests/functional/parallel/index.test.ts --detectOpenHandles --forceExit --verbose",
34
35
  "test:sequence": "NODE_ENV=test jest ./tests/functional/sequence/index.test.ts --detectOpenHandles --forceExit --verbose",
35
36
  "test:quorum": "NODE_ENV=test jest ./tests/functional/quorum/index.test.ts --detectOpenHandles --forceExit --verbose",
@@ -46,6 +47,7 @@
46
47
  "test:durable": "NODE_ENV=test jest ./tests/durable/*/index.test.ts --detectOpenHandles --forceExit --verbose",
47
48
  "test:durable:meshos": "NODE_ENV=test jest ./tests/durable/meshos/index.test.ts --detectOpenHandles --forceExit --verbose",
48
49
  "test:durable:hello": "NODE_ENV=test jest ./tests/durable/helloworld/index.test.ts --detectOpenHandles --forceExit --verbose",
50
+ "test:durable:interrupt": "NODE_ENV=test jest ./tests/durable/interrupt/index.test.ts --detectOpenHandles --forceExit --verbose",
49
51
  "test:durable:goodbye": "NODE_ENV=test jest ./tests/durable/goodbye/index.test.ts --detectOpenHandles --forceExit --verbose",
50
52
  "test:durable:hook": "NODE_ENV=test jest ./tests/durable/hook/index.test.ts --detectOpenHandles --forceExit --verbose",
51
53
  "test:durable:retry": "NODE_ENV=test jest ./tests/durable/retry/index.test.ts --detectOpenHandles --forceExit --verbose",
@@ -58,5 +58,10 @@ declare class Activity {
58
58
  resolveAdjacentDad(): string;
59
59
  filterAdjacent(): Promise<StreamData[]>;
60
60
  transition(adjacencyList: StreamData[], jobStatus: JobStatus): Promise<string[]>;
61
+ /**
62
+ * A job with a vale < -100_000_000 is considered interrupted,
63
+ * as the interruption event decrements the job status by 1billion.
64
+ */
65
+ jobWasInterrupted(jobStatus: JobStatus): boolean;
61
66
  }
62
67
  export { Activity, ActivityType };
@@ -9,6 +9,7 @@ const pipe_1 = require("../pipe");
9
9
  const serializer_1 = require("../serializer");
10
10
  const telemetry_1 = require("../telemetry");
11
11
  const stream_1 = require("../../types/stream");
12
+ const enums_1 = require("../../modules/enums");
12
13
  /**
13
14
  * The base class for all activities
14
15
  */
@@ -44,6 +45,7 @@ class Activity {
44
45
  let telemetry;
45
46
  try {
46
47
  await this.getState();
48
+ collator_1.CollatorService.assertJobActive(this.context.metadata.js, this.context.metadata.jid, this.metadata.aid);
47
49
  const aState = await collator_1.CollatorService.notarizeReentry(this);
48
50
  this.adjacentIndex = collator_1.CollatorService.getDimensionalIndex(aState);
49
51
  telemetry = new telemetry_1.TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
@@ -71,12 +73,20 @@ class Activity {
71
73
  this.logger.info('process-event-inactive-error', { error });
72
74
  return;
73
75
  }
76
+ else if (error instanceof errors_1.InactiveJobError) {
77
+ this.logger.info('process-event-inactive-job-error', { error });
78
+ return;
79
+ }
80
+ else if (error instanceof errors_1.GetStateError) {
81
+ this.logger.info('process-event-get-job-error', { error });
82
+ return;
83
+ }
74
84
  this.logger.error('activity-process-event-error', { error });
75
85
  telemetry && telemetry.setActivityError(error.message);
76
86
  throw error;
77
87
  }
78
88
  finally {
79
- telemetry && telemetry.endActivitySpan();
89
+ telemetry?.endActivitySpan();
80
90
  this.logger.debug('activity-process-event-end', { jid, aid });
81
91
  }
82
92
  }
@@ -113,6 +123,7 @@ class Activity {
113
123
  telemetry.mapActivityAttributes();
114
124
  const jobStatus = this.resolveStatus(multiResponse);
115
125
  const attrs = { 'app.job.jss': jobStatus };
126
+ //adjacencyList membership has already been set at this point (according to activity status)
116
127
  const messageIds = await this.transition(this.adjacencyList, jobStatus);
117
128
  if (messageIds.length) {
118
129
  attrs['app.activity.mids'] = messageIds.join(',');
@@ -307,11 +318,12 @@ class Activity {
307
318
  self.hook = {};
308
319
  }
309
320
  context['$self'] = self;
310
- context['$job'] = context; //NEVER call STRINGIFY! (circular)
321
+ context['$job'] = context; //NEVER call STRINGIFY! (now circular)
311
322
  return context;
312
323
  }
313
324
  initPolicies(context) {
314
- context.metadata.expire = this.config.expire;
325
+ const expire = pipe_1.Pipe.resolve(this.config.expire ?? enums_1.EXPIRE_DURATION, context);
326
+ context.metadata.expire = expire;
315
327
  }
316
328
  bindActivityData(type) {
317
329
  this.context[this.metadata.aid][type].data = this.data;
@@ -341,6 +353,7 @@ class Activity {
341
353
  if (mapper_1.MapperService.evaluate(transitionRule, this.context, this.code)) {
342
354
  adjacencyList.push({
343
355
  metadata: {
356
+ guid: (0, utils_1.guid)(),
344
357
  jid: this.context.metadata.jid,
345
358
  dad: adjacentDad,
346
359
  aid: toActivityId,
@@ -356,15 +369,16 @@ class Activity {
356
369
  return adjacencyList;
357
370
  }
358
371
  async transition(adjacencyList, jobStatus) {
372
+ if (this.jobWasInterrupted(jobStatus)) {
373
+ return;
374
+ }
359
375
  let mIds = [];
360
376
  let emit = false;
361
377
  if (this.config.emit) {
362
378
  emit = pipe_1.Pipe.resolve(this.config.emit, this.context);
363
379
  }
364
380
  if (jobStatus <= 0 || emit) {
365
- //activity should not send 'emit' if the job is truly over
366
- const isTrueEmit = jobStatus > 0;
367
- await this.engine.runJobCompletionTasks(this.context, isTrueEmit);
381
+ await this.engine.runJobCompletionTasks(this.context, { emit: jobStatus > 0 });
368
382
  }
369
383
  if (adjacencyList.length && jobStatus > 0) {
370
384
  const multi = this.store.getMulti();
@@ -375,5 +389,12 @@ class Activity {
375
389
  }
376
390
  return mIds;
377
391
  }
392
+ /**
393
+ * A job with a vale < -100_000_000 is considered interrupted,
394
+ * as the interruption event decrements the job status by 1billion.
395
+ */
396
+ jobWasInterrupted(jobStatus) {
397
+ return jobStatus < -100000000;
398
+ }
378
399
  }
379
400
  exports.Activity = Activity;
@@ -7,6 +7,7 @@ const collator_1 = require("../collator");
7
7
  const stream_1 = require("../../types/stream");
8
8
  const telemetry_1 = require("../telemetry");
9
9
  const pipe_1 = require("../pipe");
10
+ const utils_1 = require("../../modules/utils");
10
11
  class Await extends activity_1.Activity {
11
12
  constructor(config, data, metadata, hook, engine, context) {
12
13
  super(config, data, metadata, hook, engine, context);
@@ -20,6 +21,7 @@ class Await extends activity_1.Activity {
20
21
  this.setLeg(1);
21
22
  await collator_1.CollatorService.notarizeEntry(this);
22
23
  await this.getState();
24
+ collator_1.CollatorService.assertJobActive(this.context.metadata.js, this.context.metadata.jid, this.metadata.aid);
23
25
  telemetry = new telemetry_1.TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
24
26
  telemetry.startActivitySpan(this.leg);
25
27
  this.mapInputData();
@@ -41,17 +43,22 @@ class Await extends activity_1.Activity {
41
43
  return this.context.metadata.aid;
42
44
  }
43
45
  catch (error) {
44
- telemetry.setActivityError(error.message);
45
- if (error instanceof errors_1.GetStateError) {
46
+ if (error instanceof errors_1.InactiveJobError) {
47
+ this.logger.error('await-inactive-job-error', { error });
48
+ return;
49
+ }
50
+ else if (error instanceof errors_1.GetStateError) {
46
51
  this.logger.error('await-get-state-error', { error });
52
+ return;
47
53
  }
48
54
  else {
49
55
  this.logger.error('await-process-error', { error });
50
56
  }
57
+ telemetry.setActivityError(error.message);
51
58
  throw error;
52
59
  }
53
60
  finally {
54
- telemetry.endActivitySpan();
61
+ telemetry?.endActivitySpan();
55
62
  this.logger.debug('await-process-end', { jid: this.context.metadata.jid, aid: this.metadata.aid });
56
63
  }
57
64
  }
@@ -59,6 +66,7 @@ class Await extends activity_1.Activity {
59
66
  const topic = pipe_1.Pipe.resolve(this.config.subtype, this.context);
60
67
  const streamData = {
61
68
  metadata: {
69
+ guid: (0, utils_1.guid)(),
62
70
  jid: this.context.metadata.jid,
63
71
  dad: this.metadata.dad,
64
72
  aid: this.metadata.aid,
@@ -5,6 +5,7 @@ const errors_1 = require("../../modules/errors");
5
5
  const collator_1 = require("../collator");
6
6
  const activity_1 = require("./activity");
7
7
  const telemetry_1 = require("../telemetry");
8
+ const utils_1 = require("../../modules/utils");
8
9
  class Cycle extends activity_1.Activity {
9
10
  constructor(config, data, metadata, hook, engine, context) {
10
11
  super(config, data, metadata, hook, engine, context);
@@ -18,6 +19,7 @@ class Cycle extends activity_1.Activity {
18
19
  this.setLeg(1);
19
20
  await collator_1.CollatorService.notarizeEntry(this);
20
21
  await this.getState();
22
+ collator_1.CollatorService.assertJobActive(this.context.metadata.js, this.context.metadata.jid, this.metadata.aid);
21
23
  telemetry = new telemetry_1.TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
22
24
  telemetry.startActivitySpan(this.leg);
23
25
  this.mapInputData();
@@ -41,8 +43,13 @@ class Cycle extends activity_1.Activity {
41
43
  return this.context.metadata.aid;
42
44
  }
43
45
  catch (error) {
44
- if (error instanceof errors_1.GetStateError) {
46
+ if (error instanceof errors_1.InactiveJobError) {
47
+ this.logger.error('cycle-inactive-job-error', { error });
48
+ return;
49
+ }
50
+ else if (error instanceof errors_1.GetStateError) {
45
51
  this.logger.error('cycle-get-state-error', { error });
52
+ return;
46
53
  }
47
54
  else {
48
55
  this.logger.error('cycle-process-error', { error });
@@ -51,7 +58,7 @@ class Cycle extends activity_1.Activity {
51
58
  throw error;
52
59
  }
53
60
  finally {
54
- telemetry.endActivitySpan();
61
+ telemetry?.endActivitySpan();
55
62
  this.logger.debug('cycle-process-end', { jid: this.context.metadata.jid, aid: this.metadata.aid });
56
63
  }
57
64
  }
@@ -70,6 +77,7 @@ class Cycle extends activity_1.Activity {
70
77
  this.mapInputData();
71
78
  const streamData = {
72
79
  metadata: {
80
+ guid: (0, utils_1.guid)(),
73
81
  dad: collator_1.CollatorService.resolveReentryDimension(this),
74
82
  jid: this.context.metadata.jid,
75
83
  aid: this.config.ancestor,
@@ -23,6 +23,7 @@ class Hook extends activity_1.Activity {
23
23
  this.setLeg(1);
24
24
  await collator_1.CollatorService.notarizeEntry(this);
25
25
  await this.getState();
26
+ collator_1.CollatorService.assertJobActive(this.context.metadata.js, this.context.metadata.jid, this.metadata.aid);
26
27
  telemetry = new telemetry_1.TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
27
28
  telemetry.startActivitySpan(this.leg);
28
29
  let multiResponse;
@@ -59,8 +60,13 @@ class Hook extends activity_1.Activity {
59
60
  return this.context.metadata.aid;
60
61
  }
61
62
  catch (error) {
62
- if (error instanceof errors_1.GetStateError) {
63
+ if (error instanceof errors_1.InactiveJobError) {
64
+ this.logger.error('hook-inactive-job-error', { error });
65
+ return;
66
+ }
67
+ else if (error instanceof errors_1.GetStateError) {
63
68
  this.logger.error('hook-get-state-error', { error });
69
+ return;
64
70
  }
65
71
  else {
66
72
  this.logger.error('hook-process-error', { error });
@@ -69,7 +75,7 @@ class Hook extends activity_1.Activity {
69
75
  throw error;
70
76
  }
71
77
  finally {
72
- telemetry.endActivitySpan();
78
+ telemetry?.endActivitySpan();
73
79
  this.logger.debug('hook-process-end', { jid: this.context.metadata.jid, aid: this.metadata.aid });
74
80
  }
75
81
  }
@@ -2,7 +2,7 @@ import { Activity } from './activity';
2
2
  import { Await } from './await';
3
3
  import { Cycle } from './cycle';
4
4
  import { Hook } from './hook';
5
- import { Iterate } from './iterate';
5
+ import { Interrupt } from './interrupt';
6
6
  import { Signal } from './signal';
7
7
  import { Trigger } from './trigger';
8
8
  import { Worker } from './worker';
@@ -11,7 +11,7 @@ declare const _default: {
11
11
  await: typeof Await;
12
12
  cycle: typeof Cycle;
13
13
  hook: typeof Hook;
14
- iterate: typeof Iterate;
14
+ interrupt: typeof Interrupt;
15
15
  signal: typeof Signal;
16
16
  trigger: typeof Trigger;
17
17
  worker: typeof Worker;