@hotmeshio/hotmesh 0.1.3 → 0.1.4

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 (41) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  3. package/README.md +15 -12
  4. package/build/modules/enums.d.ts +1 -0
  5. package/build/modules/enums.js +2 -1
  6. package/build/modules/key.d.ts +1 -2
  7. package/build/modules/key.js +3 -4
  8. package/build/modules/utils.js +10 -11
  9. package/build/package.json +3 -2
  10. package/build/services/activities/trigger.d.ts +2 -1
  11. package/build/services/activities/trigger.js +20 -8
  12. package/build/services/compiler/deployer.js +1 -1
  13. package/build/services/durable/client.js +5 -1
  14. package/build/services/durable/worker.js +1 -1
  15. package/build/services/engine/index.d.ts +1 -1
  16. package/build/services/engine/index.js +13 -6
  17. package/build/services/hotmesh/index.js +13 -1
  18. package/build/services/router/index.d.ts +2 -0
  19. package/build/services/router/index.js +37 -15
  20. package/build/services/store/clients/ioredis.js +28 -8
  21. package/build/services/store/clients/redis.js +27 -10
  22. package/build/services/store/index.d.ts +6 -2
  23. package/build/services/store/index.js +37 -5
  24. package/build/services/stream/clients/ioredis.js +1 -1
  25. package/build/services/stream/clients/redis.js +23 -23
  26. package/build/services/worker/index.d.ts +1 -1
  27. package/build/services/worker/index.js +4 -2
  28. package/build/types/durable.d.ts +11 -0
  29. package/build/types/hotmesh.d.ts +1 -1
  30. package/build/types/hotmesh.js +1 -1
  31. package/build/types/job.d.ts +11 -0
  32. package/build/types/quorum.d.ts +2 -4
  33. package/build/types/redis.d.ts +3 -0
  34. package/build/types/stream.d.ts +2 -0
  35. package/package.json +3 -2
  36. package/types/durable.ts +14 -1
  37. package/types/hotmesh.ts +1 -1
  38. package/types/job.ts +12 -0
  39. package/types/quorum.ts +2 -4
  40. package/types/redis.ts +3 -0
  41. package/types/stream.ts +2 -0
@@ -0,0 +1,38 @@
1
+ ---
2
+ name: Bug report
3
+ about: Create a report to help us improve
4
+ title: ''
5
+ labels: ''
6
+ assignees: ''
7
+
8
+ ---
9
+
10
+ **Describe the bug**
11
+ A clear and concise description of what the bug is.
12
+
13
+ **To Reproduce**
14
+ Steps to reproduce the behavior:
15
+ 1. Go to '...'
16
+ 2. Click on '....'
17
+ 3. Scroll down to '....'
18
+ 4. See error
19
+
20
+ **Expected behavior**
21
+ A clear and concise description of what you expected to happen.
22
+
23
+ **Screenshots**
24
+ If applicable, add screenshots to help explain your problem.
25
+
26
+ **Desktop (please complete the following information):**
27
+ - OS: [e.g. iOS]
28
+ - Browser [e.g. chrome, safari]
29
+ - Version [e.g. 22]
30
+
31
+ **Smartphone (please complete the following information):**
32
+ - Device: [e.g. iPhone6]
33
+ - OS: [e.g. iOS8.1]
34
+ - Browser [e.g. stock browser, safari]
35
+ - Version [e.g. 22]
36
+
37
+ **Additional context**
38
+ Add any other context about the problem here.
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: Feature request
3
+ about: Suggest an idea for this project
4
+ title: ''
5
+ labels: ''
6
+ assignees: ''
7
+
8
+ ---
9
+
10
+ **Is your feature request related to a problem? Please describe.**
11
+ A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12
+
13
+ **Describe the solution you'd like**
14
+ A clear and concise description of what you want to happen.
15
+
16
+ **Describe alternatives you've considered**
17
+ A clear and concise description of any alternative solutions or features you've considered.
18
+
19
+ **Additional context**
20
+ Add any other context or screenshots about the feature request here.
package/README.md CHANGED
@@ -185,7 +185,7 @@ Externalizing state fundamentally changes the execution profile for your functio
185
185
  const value = await search.mult('name', 12);
186
186
  ```
187
187
 
188
- Refer to the [hotmeshio/samples-typescript](https://github.com/hotmeshio/samples-typescript) repo for usage examples.
188
+ Refer to the [hotmeshio/samples-javascript](https://github.com/hotmeshio/samples-javascript) repo for usage examples.
189
189
 
190
190
  ### Design | Advanced
191
191
  The *Pluck* and *Durable* modules are the easiest way to use HotMesh. 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.
@@ -295,28 +295,31 @@ const hotMesh = await HotMesh.init({
295
295
  ```
296
296
 
297
297
  ### Observability
298
- 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.
298
+ 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.
299
299
 
300
300
  ## FAQ
301
- 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.
301
+ 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.
302
302
 
303
303
  ## Quick Start
304
- Refer to the [Quick Start](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/quickstart.md) for sample YAML workflows you can copy, paste, and modify to get started.
304
+ Refer to the [Quick Start](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/quickstart.md) for sample YAML workflows you can copy, paste, and modify to get started.
305
305
 
306
306
  ## Developer Guide
307
- 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).
307
+ 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).
308
308
 
309
309
  ## Model Driven Development
310
- [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.
310
+ [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.
311
311
 
312
312
  ## Data Mapping
313
- 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).
313
+ 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).
314
314
 
315
315
  ## Composition
316
- 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).
317
-
318
- ## Distributed Orchestration
319
- 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.
316
+ 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).
320
317
 
321
318
  ## System Lifecycle
322
- 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).
319
+ 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).
320
+
321
+ ## Distributed Orchestration | System Overview
322
+ 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 high-level overview of the approach.
323
+
324
+ ## Distributed Orchestration | System Design
325
+ HotMesh is more than Redis and TypeScript. The theory that underlies the architecture is applicable to any number of data storage and streaming backends: [A Message-Oriented Approach to Decentralized Process Orchestration](https://zenodo.org/records/12168558).
@@ -23,6 +23,7 @@ export declare const HMSH_QUORUM_DELAY_MS = 250;
23
23
  export declare const HMSH_ACTIVATION_MAX_RETRY = 3;
24
24
  export declare const HMSH_OTT_WAIT_TIME: number;
25
25
  export declare const HMSH_EXPIRE_JOB_SECONDS: number;
26
+ export declare const MAX_DELAY = 2147483647;
26
27
  export declare const HMSH_MAX_RETRIES: number;
27
28
  export declare const HMSH_MAX_TIMEOUT_MS: number;
28
29
  export declare const HMSH_GRADUATED_INTERVAL_MS: number;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HMSH_GUID_SIZE = exports.HMSH_SCOUT_INTERVAL_SECONDS = exports.HMSH_FIDELITY_SECONDS = exports.HMSH_EXPIRE_DURATION = exports.HMSH_XPENDING_COUNT = exports.HMSH_XCLAIM_COUNT = exports.HMSH_XCLAIM_DELAY_MS = exports.HMSH_BLOCK_TIME_MS = exports.HMSH_DURABLE_EXP_BACKOFF = exports.HMSH_DURABLE_MAX_INTERVAL = exports.HMSH_DURABLE_MAX_ATTEMPTS = exports.HMSH_GRADUATED_INTERVAL_MS = exports.HMSH_MAX_TIMEOUT_MS = exports.HMSH_MAX_RETRIES = exports.HMSH_EXPIRE_JOB_SECONDS = exports.HMSH_OTT_WAIT_TIME = exports.HMSH_ACTIVATION_MAX_RETRY = exports.HMSH_QUORUM_DELAY_MS = exports.HMSH_QUORUM_ROLLCALL_CYCLES = exports.HMSH_STATUS_UNKNOWN = exports.HMSH_CODE_DURABLE_RETRYABLE = exports.HMSH_CODE_DURABLE_FATAL = exports.HMSH_CODE_DURABLE_MAXED = exports.HMSH_CODE_DURABLE_TIMEOUT = exports.HMSH_CODE_DURABLE_WAIT = exports.HMSH_CODE_DURABLE_PROXY = exports.HMSH_CODE_DURABLE_CHILD = exports.HMSH_CODE_DURABLE_ALL = exports.HMSH_CODE_DURABLE_SLEEP = exports.HMSH_CODE_UNACKED = exports.HMSH_CODE_TIMEOUT = exports.HMSH_CODE_UNKNOWN = exports.HMSH_CODE_INTERRUPT = exports.HMSH_CODE_NOTFOUND = exports.HMSH_CODE_PENDING = exports.HMSH_CODE_SUCCESS = exports.HMSH_IS_CLUSTER = exports.HMSH_LOGLEVEL = void 0;
3
+ exports.HMSH_GUID_SIZE = exports.HMSH_SCOUT_INTERVAL_SECONDS = exports.HMSH_FIDELITY_SECONDS = exports.HMSH_EXPIRE_DURATION = exports.HMSH_XPENDING_COUNT = exports.HMSH_XCLAIM_COUNT = exports.HMSH_XCLAIM_DELAY_MS = exports.HMSH_BLOCK_TIME_MS = exports.HMSH_DURABLE_EXP_BACKOFF = exports.HMSH_DURABLE_MAX_INTERVAL = exports.HMSH_DURABLE_MAX_ATTEMPTS = exports.HMSH_GRADUATED_INTERVAL_MS = exports.HMSH_MAX_TIMEOUT_MS = exports.HMSH_MAX_RETRIES = exports.MAX_DELAY = exports.HMSH_EXPIRE_JOB_SECONDS = exports.HMSH_OTT_WAIT_TIME = exports.HMSH_ACTIVATION_MAX_RETRY = exports.HMSH_QUORUM_DELAY_MS = exports.HMSH_QUORUM_ROLLCALL_CYCLES = exports.HMSH_STATUS_UNKNOWN = exports.HMSH_CODE_DURABLE_RETRYABLE = exports.HMSH_CODE_DURABLE_FATAL = exports.HMSH_CODE_DURABLE_MAXED = exports.HMSH_CODE_DURABLE_TIMEOUT = exports.HMSH_CODE_DURABLE_WAIT = exports.HMSH_CODE_DURABLE_PROXY = exports.HMSH_CODE_DURABLE_CHILD = exports.HMSH_CODE_DURABLE_ALL = exports.HMSH_CODE_DURABLE_SLEEP = exports.HMSH_CODE_UNACKED = exports.HMSH_CODE_TIMEOUT = exports.HMSH_CODE_UNKNOWN = exports.HMSH_CODE_INTERRUPT = exports.HMSH_CODE_NOTFOUND = exports.HMSH_CODE_PENDING = exports.HMSH_CODE_SUCCESS = exports.HMSH_IS_CLUSTER = exports.HMSH_LOGLEVEL = void 0;
4
4
  // HOTMESH SYSTEM
5
5
  exports.HMSH_LOGLEVEL = process.env.HMSH_LOGLEVEL || 'info';
6
6
  exports.HMSH_IS_CLUSTER = process.env.HMSH_IS_CLUSTER === 'true';
@@ -32,6 +32,7 @@ exports.HMSH_ACTIVATION_MAX_RETRY = 3;
32
32
  exports.HMSH_OTT_WAIT_TIME = parseInt(process.env.HMSH_OTT_WAIT_TIME, 10) || 1000;
33
33
  exports.HMSH_EXPIRE_JOB_SECONDS = parseInt(process.env.HMSH_EXPIRE_JOB_SECONDS, 10) || 1;
34
34
  // STREAM ROUTER
35
+ exports.MAX_DELAY = 2147483647; // Maximum allowed delay in milliseconds for setTimeout
35
36
  exports.HMSH_MAX_RETRIES = parseInt(process.env.HMSH_MAX_RETRIES, 10) || 3;
36
37
  exports.HMSH_MAX_TIMEOUT_MS = parseInt(process.env.HMSH_MAX_TIMEOUT_MS, 10) || 60000;
37
38
  exports.HMSH_GRADUATED_INTERVAL_MS = parseInt(process.env.HMSH_GRADUATED_INTERVAL_MS, 10) || 5000;
@@ -4,14 +4,13 @@ import { KeyStoreParams, KeyType } from '../types/hotmesh';
4
4
  *
5
5
  * hmsh -> {hash} hotmesh config {version: "0.0.1", namespace: "hmsh"}
6
6
  * hmsh:a:<appid> -> {hash} app profile { "id": "appid", "version": "2", "versions/1": "GMT", "versions/2": "GMT"}
7
- * hmsh:<appid>:e:<engineId> -> {string} setnx to ensure only one engine of given id
7
+ * hmsh:<appid>:r: -> {hash} throttle rates {':': '23', 'topic.thing': '555'} => {':i': 'all', 'topic.thing': '555seconds'}
8
8
  * hmsh:<appid>:w: -> {zset} work items/tasks an engine must do like garbage collect or hook a set of matching records (hookAll)
9
9
  * hmsh:<appid>:t: -> {zset} an ordered set of list (work lists) ids
10
10
  * hmsh:<appid>:t:<timeValue?> -> {list} a worklist of `jobId+activityId` items that should be awakened
11
11
  * hmsh:<appid>:q: -> {hash} quorum-wide messages
12
12
  * hmsh:<appid>:q:<ngnid> -> {hash} engine-targeted messages (targeted quorum-oriented message)
13
13
  * hmsh:<appid>:j:<jobid> -> {hash} job data
14
- * hmsh:<appid>:j:<jobid>:<activityid> -> {hash} job activity data (a1)
15
14
  * hmsh:<appid>:s:<jobkey>:<dateTime> -> {hash} job stats (general)
16
15
  * hmsh:<appid>:s:<jobkey>:<dateTime>:mdn:<field/path>:<fieldvalue> -> {zset} job stats (median)
17
16
  * hmsh:<appid>:s:<jobkey>:<dateTime>:index:<field/path>:<fieldvalue> -> {list} job stats (index of jobid[])
@@ -8,14 +8,13 @@ Object.defineProperty(exports, "KeyType", { enumerable: true, get: function () {
8
8
  *
9
9
  * hmsh -> {hash} hotmesh config {version: "0.0.1", namespace: "hmsh"}
10
10
  * hmsh:a:<appid> -> {hash} app profile { "id": "appid", "version": "2", "versions/1": "GMT", "versions/2": "GMT"}
11
- * hmsh:<appid>:e:<engineId> -> {string} setnx to ensure only one engine of given id
11
+ * hmsh:<appid>:r: -> {hash} throttle rates {':': '23', 'topic.thing': '555'} => {':i': 'all', 'topic.thing': '555seconds'}
12
12
  * hmsh:<appid>:w: -> {zset} work items/tasks an engine must do like garbage collect or hook a set of matching records (hookAll)
13
13
  * hmsh:<appid>:t: -> {zset} an ordered set of list (work lists) ids
14
14
  * hmsh:<appid>:t:<timeValue?> -> {list} a worklist of `jobId+activityId` items that should be awakened
15
15
  * hmsh:<appid>:q: -> {hash} quorum-wide messages
16
16
  * hmsh:<appid>:q:<ngnid> -> {hash} engine-targeted messages (targeted quorum-oriented message)
17
17
  * hmsh:<appid>:j:<jobid> -> {hash} job data
18
- * hmsh:<appid>:j:<jobid>:<activityid> -> {hash} job activity data (a1)
19
18
  * hmsh:<appid>:s:<jobkey>:<dateTime> -> {hash} job stats (general)
20
19
  * hmsh:<appid>:s:<jobkey>:<dateTime>:mdn:<field/path>:<fieldvalue> -> {zset} job stats (median)
21
20
  * hmsh:<appid>:s:<jobkey>:<dateTime>:index:<field/path>:<fieldvalue> -> {list} job stats (index of jobid[])
@@ -56,8 +55,8 @@ class KeyService {
56
55
  switch (keyType) {
57
56
  case hotmesh_1.KeyType.HOTMESH:
58
57
  return namespace;
59
- case hotmesh_1.KeyType.ENGINE_ID:
60
- return `${namespace}:${params.appId}:e:${params.engineId}`;
58
+ case hotmesh_1.KeyType.THROTTLE_RATE:
59
+ return `${namespace}:${params.appId}:r:`;
61
60
  case hotmesh_1.KeyType.WORK_ITEMS:
62
61
  return `${namespace}:${params.appId}:w:${params.scoutType || ''}`;
63
62
  case hotmesh_1.KeyType.TIME_RANGE:
@@ -5,7 +5,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  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.deepCopy = exports.sleepImmediate = exports.sleepFor = exports.getSystemHealth = void 0;
7
7
  const os_1 = __importDefault(require("os"));
8
- const systeminformation_1 = __importDefault(require("systeminformation"));
9
8
  const nanoid_1 = require("nanoid");
10
9
  const enums_1 = require("./enums");
11
10
  async function safeExecute(operation, defaultValue) {
@@ -21,23 +20,23 @@ async function getSystemHealth() {
21
20
  const totalMemory = os_1.default.totalmem();
22
21
  const freeMemory = os_1.default.freemem();
23
22
  const usedMemory = totalMemory - freeMemory;
24
- const cpus = os_1.default.cpus();
23
+ //const cpus = os.cpus();
25
24
  // CPU load calculation remains unchanged
26
- const cpuLoad = cpus.map((cpu, i) => {
27
- const total = Object.values(cpu.times).reduce((acc, tv) => acc + tv, 0);
28
- const idle = cpu.times.idle;
29
- const usage = ((total - idle) / total) * 100;
30
- return { [`CPU ${i} Usage`]: `${usage.toFixed(2)}%` };
31
- });
25
+ // const cpuLoad = cpus.map((cpu, i) => {
26
+ // const total = Object.values(cpu.times).reduce((acc, tv) => acc + tv, 0);
27
+ // const idle = cpu.times.idle;
28
+ // const usage = ((total - idle) / total) * 100;
29
+ // return { [`CPU ${i} Usage`]: `${usage.toFixed(2)}%` };
30
+ // });
32
31
  // Wrap each systeminformation call with safeExecute
33
- const networkStats = await safeExecute(systeminformation_1.default.networkStats(), []);
32
+ //const networkStats = await safeExecute(si.networkStats(), []);
34
33
  // Construct the system health object with error handling in mind
35
34
  const systemHealth = {
36
35
  TotalMemoryGB: `${(totalMemory / 1024 / 1024 / 1024).toFixed(2)} GB`,
37
36
  FreeMemoryGB: `${(freeMemory / 1024 / 1024 / 1024).toFixed(2)} GB`,
38
37
  UsedMemoryGB: `${(usedMemory / 1024 / 1024 / 1024).toFixed(2)} GB`,
39
- CPULoad: cpuLoad,
40
- NetworkStats: networkStats,
38
+ CPULoad: [],
39
+ NetworkStats: [],
41
40
  };
42
41
  return systemHealth;
43
42
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -31,7 +31,7 @@
31
31
  "test:durable:collision": "NODE_ENV=test jest ./tests/durable/collision/index.test.ts --detectOpenHandles --forceExit --verbose",
32
32
  "test:durable:fatal": "NODE_ENV=test jest ./tests/durable/fatal/index.test.ts --detectOpenHandles --forceExit --verbose",
33
33
  "test:durable:goodbye": "NODE_ENV=test jest ./tests/durable/goodbye/index.test.ts --detectOpenHandles --forceExit --verbose",
34
- "test:durable:hello": "NODE_ENV=test jest ./tests/durable/helloworld/index.test.ts --detectOpenHandles --forceExit --verbose",
34
+ "test:durable:hello": "HMSH_IS_CLUSTER=true NODE_ENV=test jest ./tests/durable/helloworld/index.test.ts --detectOpenHandles --forceExit --verbose",
35
35
  "test:durable:hook": "NODE_ENV=test jest ./tests/durable/hook/index.test.ts --detectOpenHandles --forceExit --verbose",
36
36
  "test:durable:interrupt": "NODE_ENV=test jest ./tests/durable/interrupt/index.test.ts --detectOpenHandles --forceExit --verbose",
37
37
  "test:durable:loopactivity": "NODE_ENV=test jest ./tests/durable/loopactivity/index.test.ts --detectOpenHandles --forceExit --verbose",
@@ -41,6 +41,7 @@
41
41
  "test:durable:signal": "NODE_ENV=test jest ./tests/durable/signal/index.test.ts --detectOpenHandles --forceExit --verbose",
42
42
  "test:durable:unknown": "NODE_ENV=test jest ./tests/durable/unknown/index.test.ts --detectOpenHandles --forceExit --verbose",
43
43
  "test:emit": "NODE_ENV=test jest ./tests/functional/emit/index.test.ts --detectOpenHandles --forceExit --verbose",
44
+ "test:expired": "NODE_ENV=test jest ./tests/functional/expired/index.test.ts --detectOpenHandles --forceExit --verbose",
44
45
  "test:functional": "NODE_ENV=test jest ./tests/functional/*/index.test.ts --detectOpenHandles --forceExit --verbose",
45
46
  "test:hmsh": "NODE_ENV=test jest ./tests/functional/index.test.ts --detectOpenHandles --verbose",
46
47
  "test:hook": "NODE_ENV=test jest ./tests/functional/hook/index.test.ts --detectOpenHandles --forceExit --verbose",
@@ -7,6 +7,7 @@ declare class Trigger extends Activity {
7
7
  config: TriggerActivity;
8
8
  constructor(config: ActivityType, data: ActivityData, metadata: ActivityMetadata, hook: ActivityData | null, engine: EngineService, context?: JobState);
9
9
  process(options?: ExtensionType): Promise<string>;
10
+ setExpired(seconds: number, multi: RedisMulti): Promise<void>;
10
11
  safeKey(key: string): string;
11
12
  bindSearchData(options?: ExtensionType): void;
12
13
  bindMarkerData(options?: ExtensionType): void;
@@ -20,7 +21,7 @@ declare class Trigger extends Activity {
20
21
  getJobStatus(): number;
21
22
  resolveJobId(context: Partial<JobState>): string;
22
23
  resolveJobKey(context: Partial<JobState>): string;
23
- setStateNX(): Promise<void>;
24
+ setStateNX(status?: number): Promise<void>;
24
25
  /**
25
26
  * Registers this job as a dependent of the parent job; when the
26
27
  * parent job is interrupted, this job will be interrupted
@@ -25,15 +25,20 @@ class Trigger extends activity_1.Activity {
25
25
  telemetry.startJobSpan();
26
26
  telemetry.startActivitySpan(this.leg);
27
27
  this.mapJobData();
28
- await this.setStateNX();
28
+ await this.setStateNX(options?.expired ? -1 : undefined);
29
29
  this.adjacencyList = await this.filterAdjacent();
30
- await this.setStatus(this.adjacencyList.length);
30
+ await this.setStatus(options?.expired ? -1 : this.adjacencyList.length);
31
31
  this.bindSearchData(options);
32
32
  this.bindMarkerData(options);
33
33
  const multi = this.store.getMulti();
34
34
  await this.setState(multi);
35
35
  await this.setStats(multi);
36
- await this.registerJobDependency(multi);
36
+ if (options?.expired) {
37
+ await this.setExpired(options?.expired, multi);
38
+ }
39
+ else {
40
+ await this.registerJobDependency(multi);
41
+ }
37
42
  await multi.exec();
38
43
  //if the parent (spawner) chose not to await,
39
44
  // emit the job_id as the data payload { job_id }
@@ -42,9 +47,13 @@ class Trigger extends activity_1.Activity {
42
47
  const jobStatus = Number(this.context.metadata.js);
43
48
  telemetry.setJobAttributes({ 'app.job.jss': jobStatus });
44
49
  const attrs = { 'app.job.jss': jobStatus };
45
- const messageIds = await this.transition(this.adjacencyList, jobStatus);
46
- if (messageIds.length) {
47
- attrs['app.activity.mids'] = messageIds.join(',');
50
+ //only transition if not inited in an expired state
51
+ //todo: enable resume from expired state
52
+ if (jobStatus !== -1) {
53
+ const messageIds = await this.transition(this.adjacencyList, jobStatus);
54
+ if (messageIds.length) {
55
+ attrs['app.activity.mids'] = messageIds.join(',');
56
+ }
48
57
  }
49
58
  telemetry.setActivityAttributes(attrs);
50
59
  return this.context.metadata.jid;
@@ -69,6 +78,9 @@ class Trigger extends activity_1.Activity {
69
78
  });
70
79
  }
71
80
  }
81
+ async setExpired(seconds, multi) {
82
+ await this.store.expireJob(this.context.metadata.jid, seconds, multi);
83
+ }
72
84
  safeKey(key) {
73
85
  return `_${key}`;
74
86
  }
@@ -190,9 +202,9 @@ class Trigger extends activity_1.Activity {
190
202
  const jobKey = this.config.stats?.key;
191
203
  return jobKey ? pipe_1.Pipe.resolve(jobKey, context) : '';
192
204
  }
193
- async setStateNX() {
205
+ async setStateNX(status) {
194
206
  const jobId = this.context.metadata.jid;
195
- if (!await this.store.setStateNX(jobId, this.engine.appId)) {
207
+ if (!await this.store.setStateNX(jobId, this.engine.appId, status)) {
196
208
  throw new errors_1.DuplicateJobError(jobId);
197
209
  }
198
210
  }
@@ -479,7 +479,7 @@ class Deployer {
479
479
  await this.store.xgroup('CREATE', stream, group, '$', 'MKSTREAM');
480
480
  }
481
481
  catch (err) {
482
- this.store.logger.info('consumer-group-exists', { stream, group });
482
+ this.store.logger.info('router-stream-group-exists', { stream, group });
483
483
  }
484
484
  }
485
485
  }
@@ -73,7 +73,11 @@ class ClientService {
73
73
  1000,
74
74
  };
75
75
  const context = { metadata: { trc, spn }, data: {} };
76
- const jobId = await hotMeshClient.pub(`${options.namespace ?? factory_1.APP_ID}.execute`, payload, context, { search: options?.search?.data, marker: options?.marker });
76
+ const jobId = await hotMeshClient.pub(`${options.namespace ?? factory_1.APP_ID}.execute`, payload, context, {
77
+ search: options?.search?.data,
78
+ marker: options?.marker,
79
+ expired: options?.expired,
80
+ });
77
81
  return new handle_1.WorkflowHandleService(hotMeshClient, workflowTopic, jobId);
78
82
  },
79
83
  /**
@@ -86,7 +86,7 @@ class WorkerService {
86
86
  }
87
87
  }
88
88
  async run() {
89
- this.workflowRunner.engine.logger.info('WorkerService is running');
89
+ this.workflowRunner.engine.logger.info('durable-worker-running');
90
90
  }
91
91
  async initActivityWorker(config, activityTopic) {
92
92
  const redisConfig = {
@@ -47,7 +47,7 @@ declare class EngineService {
47
47
  initStoreChannel(store: RedisClient): Promise<void>;
48
48
  initSubChannel(sub: RedisClient): Promise<void>;
49
49
  initStreamChannel(stream: RedisClient): Promise<void>;
50
- initRouter(config: HotMeshConfig): Router;
50
+ initRouter(config: HotMeshConfig): Promise<Router>;
51
51
  getVID(vid?: AppVID): Promise<AppVID>;
52
52
  setCacheMode(cacheMode: CacheMode, untilVersion: string): void;
53
53
  routeToSubscribers(topic: string, message: JobOutput): Promise<void>;
@@ -40,7 +40,7 @@ class EngineService {
40
40
  await instance.initStoreChannel(config.engine.store);
41
41
  await instance.initSubChannel(config.engine.sub);
42
42
  await instance.initStreamChannel(config.engine.stream);
43
- instance.router = instance.initRouter(config);
43
+ instance.router = await instance.initRouter(config);
44
44
  instance.router.consumeMessages(instance.stream.mintKey(key_1.KeyType.STREAMS, { appId: instance.appId }), 'ENGINE', instance.guid, instance.processStreamMessage.bind(instance));
45
45
  instance.taskService = new task_1.TaskService(instance.store, logger);
46
46
  instance.exporter = new exporter_1.ExporterService(instance.appId, instance.store, logger);
@@ -82,7 +82,8 @@ class EngineService {
82
82
  }
83
83
  await this.stream.init(this.namespace, this.appId, this.logger);
84
84
  }
85
- initRouter(config) {
85
+ async initRouter(config) {
86
+ const throttle = await this.store.getThrottleRate(':');
86
87
  return new router_1.Router({
87
88
  namespace: this.namespace,
88
89
  appId: this.appId,
@@ -90,6 +91,7 @@ class EngineService {
90
91
  role: stream_1.StreamRole.ENGINE,
91
92
  reclaimDelay: config.engine.reclaimDelay,
92
93
  reclaimCount: config.engine.reclaimCount,
94
+ throttle,
93
95
  }, this.stream, this.store, this.logger);
94
96
  }
95
97
  async getVID(vid) {
@@ -114,7 +116,7 @@ class EngineService {
114
116
  }
115
117
  }
116
118
  setCacheMode(cacheMode, untilVersion) {
117
- this.logger.info(`engine-rule-cache-updated`, {
119
+ this.logger.info(`engine-cache-updated`, {
118
120
  mode: cacheMode,
119
121
  until: untilVersion,
120
122
  });
@@ -135,7 +137,12 @@ class EngineService {
135
137
  this.taskService.processTimeHooks(this.hookTime.bind(this));
136
138
  }
137
139
  async throttle(delayInMillis) {
138
- this.router.setThrottle(delayInMillis);
140
+ try {
141
+ this.router.setThrottle(delayInMillis);
142
+ }
143
+ catch (e) {
144
+ this.logger.error('engine-throttle-error', { error: e });
145
+ }
139
146
  }
140
147
  // ************* METADATA/MODEL METHODS *************
141
148
  async initActivity(topic, data = {}, context) {
@@ -220,7 +227,7 @@ class EngineService {
220
227
  }
221
228
  // ****************** STREAM RE-ENTRY POINT *****************
222
229
  async processStreamMessage(streamData) {
223
- this.logger.debug('engine-process-stream-message', {
230
+ this.logger.debug('engine-process', {
224
231
  jid: streamData.metadata.jid,
225
232
  gid: streamData.metadata.gid,
226
233
  dad: streamData.metadata.dad,
@@ -279,7 +286,7 @@ class EngineService {
279
286
  const activityHandler = (await this.initActivity(`.${streamData.metadata.aid}`, streamData.data, context));
280
287
  await activityHandler.processEvent(streamData.status, streamData.code, 'output');
281
288
  }
282
- this.logger.debug('engine-process-stream-message-end', {
289
+ this.logger.debug('engine-process-end', {
283
290
  jid: streamData.metadata.jid,
284
291
  gid: streamData.metadata.gid,
285
292
  aid: streamData.metadata.aid,
@@ -11,6 +11,7 @@ const logger_1 = require("../logger");
11
11
  const quorum_1 = require("../quorum");
12
12
  const router_1 = require("../router");
13
13
  const worker_1 = require("../worker");
14
+ const enums_1 = require("../../modules/enums");
14
15
  class HotMeshService {
15
16
  constructor() {
16
17
  this.engine = null;
@@ -94,9 +95,19 @@ class HotMeshService {
94
95
  return await this.quorum?.rollCall(delay);
95
96
  }
96
97
  async throttle(options) {
98
+ let throttle;
99
+ if (options.throttle === -1) {
100
+ throttle = enums_1.MAX_DELAY;
101
+ }
102
+ else {
103
+ throttle = options.throttle;
104
+ }
105
+ if (!Number.isInteger(throttle) || throttle < 0 || throttle > enums_1.MAX_DELAY) {
106
+ throw new Error(`Throttle must be a non-negative integer and not exceed ${enums_1.MAX_DELAY} ms; send -1 to throttle indefinitely`);
107
+ }
97
108
  const throttleMessage = {
98
109
  type: 'throttle',
99
- throttle: options.throttle,
110
+ throttle: throttle,
100
111
  };
101
112
  if (options.guid) {
102
113
  throttleMessage.guid = options.guid;
@@ -104,6 +115,7 @@ class HotMeshService {
104
115
  else if (options.topic) {
105
116
  throttleMessage.topic = options.topic;
106
117
  }
118
+ await this.engine.store.setThrottleRate(throttleMessage);
107
119
  return await this.quorum?.pub(throttleMessage);
108
120
  }
109
121
  // ************* COMPILER METHODS *************
@@ -42,6 +42,8 @@ declare class Router {
42
42
  customSleep(): Promise<void>;
43
43
  consumeMessages(stream: string, group: string, consumer: string, callback: (streamData: StreamData) => Promise<StreamDataResponse | void>): Promise<void>;
44
44
  isStreamMessage(result: any): boolean;
45
+ isPaused(): boolean;
46
+ isStopped(group: string, consumer: string, stream: string): boolean;
45
47
  consumeOne(stream: string, group: string, id: string, message: string[], callback: (streamData: StreamData) => Promise<StreamDataResponse | void>): Promise<void>;
46
48
  execStreamLeg(input: StreamData, stream: string, id: string, callback: (streamData: StreamData) => Promise<StreamDataResponse | void>): Promise<StreamData>;
47
49
  ackAndDelete(stream: string, group: string, id: string): Promise<void>;
@@ -22,6 +22,7 @@ class Router {
22
22
  this.topic = config.topic;
23
23
  this.stream = stream;
24
24
  this.store = store;
25
+ this.throttle = config.throttle;
25
26
  this.reclaimDelay = config.reclaimDelay || enums_1.HMSH_XCLAIM_DELAY_MS;
26
27
  this.reclaimCount = config.reclaimCount || enums_1.HMSH_XCLAIM_COUNT;
27
28
  this.logger = logger;
@@ -38,7 +39,7 @@ class Router {
38
39
  await this.store.xgroup('CREATE', stream, group, '$', 'MKSTREAM');
39
40
  }
40
41
  catch (err) {
41
- this.logger.debug('consumer-group-exists', { stream, group });
42
+ this.logger.debug('router-stream-group-exists', { stream, group });
42
43
  }
43
44
  }
44
45
  async publishMessage(topic, streamData, multi) {
@@ -81,25 +82,31 @@ class Router {
81
82
  });
82
83
  }
83
84
  async consumeMessages(stream, group, consumer, callback) {
84
- this.logger.info(`stream-consumer-starting`, { group, consumer, stream });
85
+ this.logger.info(`router-stream-starting`, { group, consumer, stream });
85
86
  Router.instances.add(this);
86
87
  this.shouldConsume = true;
87
88
  await this.createGroup(stream, group);
88
89
  let lastCheckedPendingMessagesAt = Date.now();
89
90
  async function consume() {
90
91
  await this.customSleep();
91
- if (!this.shouldConsume) {
92
- this.logger.info(`stream-consumer-stopping`, {
93
- group,
94
- consumer,
95
- stream,
96
- });
92
+ if (this.isStopped(group, consumer, stream)) {
93
+ return;
94
+ }
95
+ else if (this.isPaused()) {
96
+ setImmediate(consume.bind(this));
97
97
  return;
98
98
  }
99
99
  try {
100
100
  //randomizer that asymptotes at 150% of `HMSH_BLOCK_TIME_MS`
101
101
  const streamDuration = enums_1.HMSH_BLOCK_TIME_MS + Math.round(enums_1.HMSH_BLOCK_TIME_MS * Math.random());
102
102
  const result = await this.stream.xreadgroup('GROUP', group, consumer, 'BLOCK', streamDuration, 'STREAMS', stream, '>');
103
+ if (this.isStopped(group, consumer, stream)) {
104
+ return;
105
+ }
106
+ else if (this.isPaused()) {
107
+ setImmediate(consume.bind(this));
108
+ return;
109
+ }
103
110
  if (this.isStreamMessage(result)) {
104
111
  const [[, messages]] = result;
105
112
  for (const [id, message] of messages) {
@@ -119,7 +126,7 @@ class Router {
119
126
  }
120
127
  catch (err) {
121
128
  if (this.shouldConsume && process.env.NODE_ENV !== 'test') {
122
- this.logger.error(`stream-consume-message-error`, {
129
+ this.logger.error(`router-stream-error`, {
123
130
  err,
124
131
  stream,
125
132
  group,
@@ -136,8 +143,21 @@ class Router {
136
143
  isStreamMessage(result) {
137
144
  return Array.isArray(result) && Array.isArray(result[0]);
138
145
  }
146
+ isPaused() {
147
+ return this.throttle === enums_1.MAX_DELAY;
148
+ }
149
+ isStopped(group, consumer, stream) {
150
+ if (!this.shouldConsume) {
151
+ this.logger.info(`router-stream-stopped`, {
152
+ group,
153
+ consumer,
154
+ stream,
155
+ });
156
+ }
157
+ return !this.shouldConsume;
158
+ }
139
159
  async consumeOne(stream, group, id, message, callback) {
140
- this.logger.debug(`stream-consume-one`, { group, stream, id });
160
+ this.logger.debug(`stream-read-one`, { group, stream, id });
141
161
  const [err, input] = this.parseStreamData(message[1]);
142
162
  let output;
143
163
  let telemetry;
@@ -151,14 +171,14 @@ class Router {
151
171
  this.errorCount = 0;
152
172
  }
153
173
  catch (err) {
154
- this.logger.error(`stream-consume-one-error`, { group, stream, id, err });
174
+ this.logger.error(`stream-read-one-error`, { group, stream, id, err });
155
175
  telemetry.setStreamError(err.message);
156
176
  }
157
177
  const messageId = await this.publishResponse(input, output);
158
178
  telemetry.setStreamAttributes({ 'app.worker.mid': messageId });
159
179
  await this.ackAndDelete(stream, group, id);
160
180
  telemetry.endStreamSpan();
161
- this.logger.debug(`stream-consume-one-end`, { group, stream, id });
181
+ this.logger.debug(`stream-read-one-end`, { group, stream, id });
162
182
  }
163
183
  async execStreamLeg(input, stream, id, callback) {
164
184
  let output;
@@ -293,7 +313,7 @@ class Router {
293
313
  }
294
314
  async stopConsuming() {
295
315
  this.shouldConsume = false;
296
- this.logger.info(`stream-consumer-stopping`, this.topic ? { topic: this.topic } : undefined);
316
+ this.logger.info(`router-stream-stopping`, this.topic ? { topic: this.topic } : undefined);
297
317
  this.cancelThrottle();
298
318
  }
299
319
  cancelThrottle() {
@@ -303,8 +323,10 @@ class Router {
303
323
  this.resetThrottleState();
304
324
  }
305
325
  setThrottle(delayInMillis) {
306
- if (!Number.isInteger(delayInMillis) || delayInMillis < 0) {
307
- throw new Error('Throttle must be a non-negative integer');
326
+ if (!Number.isInteger(delayInMillis) ||
327
+ delayInMillis < 0 ||
328
+ delayInMillis > enums_1.MAX_DELAY) {
329
+ throw new Error(`Throttle must be a non-negative integer and not exceed ${enums_1.MAX_DELAY} ms; send -1 to throttle indefinitely`);
308
330
  }
309
331
  const wasDecreased = delayInMillis < this.throttle;
310
332
  this.throttle = delayInMillis;