@hotmeshio/hotmesh 0.0.57 → 0.0.59

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 (90) hide show
  1. package/README.md +1 -1
  2. package/build/modules/enums.js +10 -1
  3. package/build/modules/key.d.ts +38 -0
  4. package/build/modules/key.js +46 -4
  5. package/build/modules/utils.d.ts +9 -0
  6. package/build/modules/utils.js +19 -1
  7. package/build/package.json +2 -1
  8. package/build/services/activities/activity.d.ts +28 -0
  9. package/build/services/activities/activity.js +46 -1
  10. package/build/services/activities/await.js +4 -0
  11. package/build/services/activities/cycle.d.ts +7 -0
  12. package/build/services/activities/cycle.js +16 -1
  13. package/build/services/activities/hook.d.ts +6 -0
  14. package/build/services/activities/hook.js +12 -2
  15. package/build/services/activities/interrupt.js +8 -0
  16. package/build/services/activities/signal.d.ts +6 -0
  17. package/build/services/activities/signal.js +15 -0
  18. package/build/services/activities/trigger.d.ts +4 -0
  19. package/build/services/activities/trigger.js +7 -1
  20. package/build/services/activities/worker.js +4 -0
  21. package/build/services/collator/index.d.ts +70 -0
  22. package/build/services/collator/index.js +91 -1
  23. package/build/services/compiler/deployer.js +38 -6
  24. package/build/services/compiler/index.d.ts +15 -0
  25. package/build/services/compiler/index.js +20 -0
  26. package/build/services/compiler/validator.d.ts +3 -0
  27. package/build/services/compiler/validator.js +25 -0
  28. package/build/services/connector/clients/ioredis.js +2 -0
  29. package/build/services/connector/clients/redis.js +2 -0
  30. package/build/services/connector/index.js +2 -0
  31. package/build/services/durable/client.d.ts +20 -0
  32. package/build/services/durable/client.js +25 -0
  33. package/build/services/durable/exporter.d.ts +22 -0
  34. package/build/services/durable/exporter.js +30 -1
  35. package/build/services/durable/handle.d.ts +36 -0
  36. package/build/services/durable/handle.js +46 -0
  37. package/build/services/durable/index.d.ts +4 -0
  38. package/build/services/durable/index.js +4 -0
  39. package/build/services/durable/schemas/factory.d.ts +29 -0
  40. package/build/services/durable/schemas/factory.js +29 -0
  41. package/build/services/durable/search.d.ts +97 -0
  42. package/build/services/durable/search.js +99 -0
  43. package/build/services/durable/worker.js +35 -6
  44. package/build/services/durable/workflow.d.ts +118 -0
  45. package/build/services/durable/workflow.js +153 -6
  46. package/build/services/engine/index.d.ts +5 -0
  47. package/build/services/engine/index.js +43 -1
  48. package/build/services/exporter/index.d.ts +27 -0
  49. package/build/services/exporter/index.js +33 -0
  50. package/build/services/hotmesh/index.js +8 -0
  51. package/build/services/logger/index.js +2 -0
  52. package/build/services/mapper/index.d.ts +14 -0
  53. package/build/services/mapper/index.js +14 -0
  54. package/build/services/pipe/functions/date.d.ts +7 -0
  55. package/build/services/pipe/functions/date.js +7 -0
  56. package/build/services/pipe/functions/math.js +2 -0
  57. package/build/services/pipe/index.d.ts +16 -0
  58. package/build/services/pipe/index.js +45 -3
  59. package/build/services/quorum/index.d.ts +7 -0
  60. package/build/services/quorum/index.js +21 -0
  61. package/build/services/reporter/index.d.ts +5 -0
  62. package/build/services/reporter/index.js +9 -0
  63. package/build/services/router/index.d.ts +9 -0
  64. package/build/services/router/index.js +30 -2
  65. package/build/services/serializer/index.js +23 -6
  66. package/build/services/store/cache.d.ts +19 -0
  67. package/build/services/store/cache.js +19 -0
  68. package/build/services/store/clients/ioredis.js +1 -0
  69. package/build/services/store/index.d.ts +55 -0
  70. package/build/services/store/index.js +81 -5
  71. package/build/services/stream/clients/ioredis.js +4 -1
  72. package/build/services/task/index.d.ts +9 -0
  73. package/build/services/task/index.js +31 -0
  74. package/build/services/telemetry/index.d.ts +7 -0
  75. package/build/services/telemetry/index.js +13 -1
  76. package/build/services/worker/index.d.ts +4 -0
  77. package/build/services/worker/index.js +6 -2
  78. package/build/types/activity.d.ts +81 -0
  79. package/build/types/durable.d.ts +255 -0
  80. package/build/types/exporter.d.ts +13 -0
  81. package/build/types/hotmesh.d.ts +10 -1
  82. package/build/types/hotmesh.js +3 -0
  83. package/build/types/index.js +1 -1
  84. package/build/types/job.d.ts +85 -0
  85. package/build/types/pipe.d.ts +65 -0
  86. package/build/types/quorum.d.ts +14 -0
  87. package/build/types/redis.d.ts +6 -0
  88. package/build/types/stream.d.ts +58 -0
  89. package/build/types/stream.js +4 -0
  90. package/package.json +2 -1
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
- HotMesh transforms Redis into a distributed orchestration engine.
4
+ HotMesh transforms Redis into an Orchestration Engine.
5
5
 
6
6
  *Write functions in your own preferred style, and let Redis govern their execution, reliably and durably.*
7
7
 
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  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_LOGLEVEL = void 0;
4
+ // HOTMESH SYSTEM
4
5
  exports.HMSH_LOGLEVEL = process.env.HMSH_LOGLEVEL || 'info';
6
+ // HOTMESH STATUS CODES
5
7
  exports.HMSH_CODE_SUCCESS = 200;
6
8
  exports.HMSH_CODE_PENDING = 202;
7
9
  exports.HMSH_CODE_NOTFOUND = 404;
@@ -9,6 +11,7 @@ exports.HMSH_CODE_INTERRUPT = 410;
9
11
  exports.HMSH_CODE_UNKNOWN = 500;
10
12
  exports.HMSH_CODE_TIMEOUT = 504;
11
13
  exports.HMSH_CODE_UNACKED = 999;
14
+ // DURABLE STATUS CODES
12
15
  exports.HMSH_CODE_DURABLE_SLEEP = 588;
13
16
  exports.HMSH_CODE_DURABLE_ALL = 589;
14
17
  exports.HMSH_CODE_DURABLE_CHILD = 590;
@@ -18,15 +21,20 @@ exports.HMSH_CODE_DURABLE_TIMEOUT = 596;
18
21
  exports.HMSH_CODE_DURABLE_MAXED = 597;
19
22
  exports.HMSH_CODE_DURABLE_FATAL = 598;
20
23
  exports.HMSH_CODE_DURABLE_RETRYABLE = 599;
24
+ // HOTMESH MESSAGES
21
25
  exports.HMSH_STATUS_UNKNOWN = 'unknown';
22
- exports.HMSH_QUORUM_ROLLCALL_CYCLES = 12;
26
+ // QUORUM
27
+ exports.HMSH_QUORUM_ROLLCALL_CYCLES = 12; //max iterations
23
28
  exports.HMSH_QUORUM_DELAY_MS = 250;
24
29
  exports.HMSH_ACTIVATION_MAX_RETRY = 3;
30
+ // ENGINE
25
31
  exports.HMSH_OTT_WAIT_TIME = parseInt(process.env.HMSH_OTT_WAIT_TIME, 10) || 1000;
26
32
  exports.HMSH_EXPIRE_JOB_SECONDS = parseInt(process.env.HMSH_EXPIRE_JOB_SECONDS, 10) || 1;
33
+ // STREAM ROUTER
27
34
  exports.HMSH_MAX_RETRIES = parseInt(process.env.HMSH_MAX_RETRIES, 10) || 3;
28
35
  exports.HMSH_MAX_TIMEOUT_MS = parseInt(process.env.HMSH_MAX_TIMEOUT_MS, 10) || 60000;
29
36
  exports.HMSH_GRADUATED_INTERVAL_MS = parseInt(process.env.HMSH_GRADUATED_INTERVAL_MS, 10) || 5000;
37
+ // DURABLE
30
38
  exports.HMSH_DURABLE_MAX_ATTEMPTS = 3;
31
39
  exports.HMSH_DURABLE_MAX_INTERVAL = '120s';
32
40
  exports.HMSH_DURABLE_EXP_BACKOFF = 10;
@@ -36,6 +44,7 @@ exports.HMSH_BLOCK_TIME_MS = process.env.HMSH_BLOCK_TIME_MS ? parseInt(process.e
36
44
  exports.HMSH_XCLAIM_DELAY_MS = parseInt(process.env.HMSH_XCLAIM_DELAY_MS, 10) || 1000 * 60;
37
45
  exports.HMSH_XCLAIM_COUNT = parseInt(process.env.HMSH_XCLAIM_COUNT, 10) || 3;
38
46
  exports.HMSH_XPENDING_COUNT = parseInt(process.env.HMSH_XPENDING_COUNT, 10) || 10;
47
+ // TASK WORKER
39
48
  exports.HMSH_EXPIRE_DURATION = parseInt(process.env.HMSH_EXPIRE_DURATION, 10) || 1;
40
49
  const BASE_FIDELITY_SECONDS = 5;
41
50
  const TEST_FIDELITY_SECONDS = 1;
@@ -1,10 +1,48 @@
1
1
  import { KeyStoreParams, KeyType } from '../types/hotmesh';
2
+ /**
3
+ * Keys
4
+ *
5
+ * hmsh -> {hash} hotmesh config {version: "0.0.1", namespace: "hmsh"}
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
8
+ * hmsh:<appid>:w: -> {zset} work items/tasks an engine must do like garbage collect or hook a set of matching records (hookAll)
9
+ * hmsh:<appid>:t: -> {zset} an ordered set of list (work lists) ids
10
+ * hmsh:<appid>:t:<timeValue?> -> {list} a worklist of `jobId+activityId` items that should be awakened
11
+ * hmsh:<appid>:q: -> {hash} quorum-wide messages
12
+ * hmsh:<appid>:q:<ngnid> -> {hash} engine-targeted messages (targeted quorum-oriented message)
13
+ * hmsh:<appid>:j:<jobid> -> {hash} job data
14
+ * hmsh:<appid>:j:<jobid>:<activityid> -> {hash} job activity data (a1)
15
+ * hmsh:<appid>:s:<jobkey>:<dateTime> -> {hash} job stats (general)
16
+ * hmsh:<appid>:s:<jobkey>:<dateTime>:mdn:<field/path>:<fieldvalue> -> {zset} job stats (median)
17
+ * hmsh:<appid>:s:<jobkey>:<dateTime>:index:<field/path>:<fieldvalue> -> {list} job stats (index of jobid[])
18
+ * hmsh:<appid>:v:<version>:activities -> {hash} schemas [cache]
19
+ * hmsh:<appid>:v:<version>:transitions -> {hash} transitions [cache]
20
+ * hmsh:<appid>:v:<version>:subscriptions -> {hash} subscriptions [cache]
21
+ * hmsh:<appid>:x: -> {xstream} when an engine is sent or reads a buffered task (engines read from their custom topic)
22
+ * hmsh:<appid>:x:<topic> -> {xstream} when a worker is sent or reads a buffered task (workers read from their custom topic)
23
+ * hmsh:<appid>:hooks -> {hash} hook patterns/rules; set at compile time
24
+ * hmsh:<appid>:signals -> {hash} dynamic hook signals (hget/hdel) when resolving (always self-clean); added/removed at runtime
25
+ * hmsh:<appid>:sym:keys: -> {hash} list of symbol ranges and :cursor assigned at version deploy time for job keys
26
+ * 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)
27
+ * hmsh:<appid>:sym:vals: -> {hash} list of symbols for job values across all app versions
28
+ */
2
29
  declare const HMNS = "hmsh";
3
30
  declare const KEYSEP = ":";
4
31
  declare const VALSEP = "::";
5
32
  declare const WEBSEP = "::";
6
33
  declare const TYPSEP = "::";
7
34
  declare class KeyService {
35
+ /**
36
+ * returns a key that can be used to access a value in the key/value store
37
+ * appropriate for the given key type; the keys have an implicit hierarchy
38
+ * and are used to organize data in the store in a tree-like structure
39
+ * via the use of colons as separators. The top-level entity is the hmsh manifest.
40
+ * This file will reveal the full scope of what is on the server (apps, versions, etc)
41
+ * @param namespace
42
+ * @param keyType
43
+ * @param params
44
+ * @returns {string}
45
+ */
8
46
  static mintKey(namespace: string, keyType: KeyType, params: KeyStoreParams): string;
9
47
  }
10
48
  export { KeyService, KeyType, KeyStoreParams, HMNS, KEYSEP, TYPSEP, WEBSEP, VALSEP };
@@ -3,17 +3,55 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.VALSEP = exports.WEBSEP = exports.TYPSEP = exports.KEYSEP = exports.HMNS = exports.KeyType = exports.KeyService = void 0;
4
4
  const hotmesh_1 = require("../types/hotmesh");
5
5
  Object.defineProperty(exports, "KeyType", { enumerable: true, get: function () { return hotmesh_1.KeyType; } });
6
+ /**
7
+ * Keys
8
+ *
9
+ * hmsh -> {hash} hotmesh config {version: "0.0.1", namespace: "hmsh"}
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
12
+ * hmsh:<appid>:w: -> {zset} work items/tasks an engine must do like garbage collect or hook a set of matching records (hookAll)
13
+ * hmsh:<appid>:t: -> {zset} an ordered set of list (work lists) ids
14
+ * hmsh:<appid>:t:<timeValue?> -> {list} a worklist of `jobId+activityId` items that should be awakened
15
+ * hmsh:<appid>:q: -> {hash} quorum-wide messages
16
+ * hmsh:<appid>:q:<ngnid> -> {hash} engine-targeted messages (targeted quorum-oriented message)
17
+ * hmsh:<appid>:j:<jobid> -> {hash} job data
18
+ * hmsh:<appid>:j:<jobid>:<activityid> -> {hash} job activity data (a1)
19
+ * hmsh:<appid>:s:<jobkey>:<dateTime> -> {hash} job stats (general)
20
+ * hmsh:<appid>:s:<jobkey>:<dateTime>:mdn:<field/path>:<fieldvalue> -> {zset} job stats (median)
21
+ * hmsh:<appid>:s:<jobkey>:<dateTime>:index:<field/path>:<fieldvalue> -> {list} job stats (index of jobid[])
22
+ * hmsh:<appid>:v:<version>:activities -> {hash} schemas [cache]
23
+ * hmsh:<appid>:v:<version>:transitions -> {hash} transitions [cache]
24
+ * hmsh:<appid>:v:<version>:subscriptions -> {hash} subscriptions [cache]
25
+ * hmsh:<appid>:x: -> {xstream} when an engine is sent or reads a buffered task (engines read from their custom topic)
26
+ * hmsh:<appid>:x:<topic> -> {xstream} when a worker is sent or reads a buffered task (workers read from their custom topic)
27
+ * hmsh:<appid>:hooks -> {hash} hook patterns/rules; set at compile time
28
+ * hmsh:<appid>:signals -> {hash} dynamic hook signals (hget/hdel) when resolving (always self-clean); added/removed at runtime
29
+ * hmsh:<appid>:sym:keys: -> {hash} list of symbol ranges and :cursor assigned at version deploy time for job keys
30
+ * 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)
31
+ * hmsh:<appid>:sym:vals: -> {hash} list of symbols for job values across all app versions
32
+ */
6
33
  const HMNS = "hmsh";
7
34
  exports.HMNS = HMNS;
8
- const KEYSEP = ':';
35
+ const KEYSEP = ':'; //default delimiter for keys
9
36
  exports.KEYSEP = KEYSEP;
10
- const VALSEP = '::';
37
+ const VALSEP = '::'; //default delimiter for vals
11
38
  exports.VALSEP = VALSEP;
12
- const WEBSEP = '::';
39
+ const WEBSEP = '::'; //default delimiter for webhook vals
13
40
  exports.WEBSEP = WEBSEP;
14
- const TYPSEP = '::';
41
+ const TYPSEP = '::'; //delimiter for ZSET task typing (how should a list be used?)
15
42
  exports.TYPSEP = TYPSEP;
16
43
  class KeyService {
44
+ /**
45
+ * returns a key that can be used to access a value in the key/value store
46
+ * appropriate for the given key type; the keys have an implicit hierarchy
47
+ * and are used to organize data in the store in a tree-like structure
48
+ * via the use of colons as separators. The top-level entity is the hmsh manifest.
49
+ * This file will reveal the full scope of what is on the server (apps, versions, etc)
50
+ * @param namespace
51
+ * @param keyType
52
+ * @param params
53
+ * @returns {string}
54
+ */
17
55
  static mintKey(namespace, keyType, params) {
18
56
  switch (keyType) {
19
57
  case hotmesh_1.KeyType.HOTMESH:
@@ -45,12 +83,16 @@ class KeyService {
45
83
  case hotmesh_1.KeyType.SUBSCRIPTION_PATTERNS:
46
84
  return `${namespace}:${params.appId}:v:${params.appVersion}:transitions`;
47
85
  case hotmesh_1.KeyType.HOOKS:
86
+ //`hooks` provide the pattern to resolve a value
48
87
  return `${namespace}:${params.appId}:hooks`;
49
88
  case hotmesh_1.KeyType.SIGNALS:
89
+ //`signals` provide the registry of resolved values that link back to paused jobs
50
90
  return `${namespace}:${params.appId}:signals`;
51
91
  case hotmesh_1.KeyType.SYMKEYS:
92
+ //`symbol keys` provide the registry of replacement values for job keys
52
93
  return `${namespace}:${params.appId}:sym:keys:${params.activityId || ''}`;
53
94
  case hotmesh_1.KeyType.SYMVALS:
95
+ //`symbol vals` provide the registry of replacement values for job vals
54
96
  return `${namespace}:${params.appId}:sym:vals:`;
55
97
  case hotmesh_1.KeyType.STREAMS:
56
98
  return `${namespace}:${params.appId || ''}:x:${params.topic || ''}`;
@@ -7,6 +7,7 @@ import { StreamCode, StreamStatus } from "../types/stream";
7
7
  import { SystemHealth } from '../types/quorum';
8
8
  export declare function getSystemHealth(): Promise<SystemHealth>;
9
9
  export declare function sleepFor(ms: number): Promise<unknown>;
10
+ export declare function sleepImmediate(): Promise<void>;
10
11
  export declare function guid(): string;
11
12
  export declare function deterministicRandom(seed: number): number;
12
13
  export declare function identifyRedisType(redisInstance: any): 'redis' | 'ioredis' | null;
@@ -22,7 +23,15 @@ export declare function XSleepFor(ms: number): {
22
23
  };
23
24
  export declare function findTopKey(obj: AppTransitions, input: string): string | null;
24
25
  export declare function findSubscriptionForTrigger(obj: AppSubscriptions, value: string): string | null;
26
+ /**
27
+ * Get the subscription topic for the flow to which @activityId belongs.
28
+ * TODO: resolve this value in the compiler...do not call this at runtime
29
+ */
25
30
  export declare function getSubscriptionTopic(activityId: string, store: StoreService<RedisClient, RedisMulti>, appVID: AppVID): Promise<string | undefined>;
31
+ /**
32
+ * returns the 12-digit format of the iso timestamp (e.g, 202101010000); returns
33
+ * an empty string if overridden by the user to not segment by time (infinity).
34
+ */
26
35
  export declare function getTimeSeries(granularity: string): string;
27
36
  export declare function formatISODate(input: Date | string): string;
28
37
  export declare function getSymKey(number: number): string;
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
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.sleepFor = exports.getSystemHealth = void 0;
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.sleepImmediate = exports.sleepFor = exports.getSystemHealth = void 0;
7
7
  const os_1 = __importDefault(require("os"));
8
8
  const systeminformation_1 = __importDefault(require("systeminformation"));
9
9
  const nanoid_1 = require("nanoid");
@@ -21,13 +21,16 @@ async function getSystemHealth() {
21
21
  const freeMemory = os_1.default.freemem();
22
22
  const usedMemory = totalMemory - freeMemory;
23
23
  const cpus = os_1.default.cpus();
24
+ // CPU load calculation remains unchanged
24
25
  const cpuLoad = cpus.map((cpu, i) => {
25
26
  const total = Object.values(cpu.times).reduce((acc, tv) => acc + tv, 0);
26
27
  const idle = cpu.times.idle;
27
28
  const usage = ((total - idle) / total) * 100;
28
29
  return { [`CPU ${i} Usage`]: `${usage.toFixed(2)}%` };
29
30
  });
31
+ // Wrap each systeminformation call with safeExecute
30
32
  const networkStats = await safeExecute(systeminformation_1.default.networkStats(), []);
33
+ // Construct the system health object with error handling in mind
31
34
  const systemHealth = {
32
35
  TotalMemoryGB: `${(totalMemory / 1024 / 1024 / 1024).toFixed(2)} GB`,
33
36
  FreeMemoryGB: `${(freeMemory / 1024 / 1024 / 1024).toFixed(2)} GB`,
@@ -42,6 +45,10 @@ async function sleepFor(ms) {
42
45
  return new Promise((resolve) => setTimeout(resolve, ms));
43
46
  }
44
47
  exports.sleepFor = sleepFor;
48
+ function sleepImmediate() {
49
+ return new Promise((resolve) => setImmediate(resolve));
50
+ }
51
+ exports.sleepImmediate = sleepImmediate;
45
52
  function guid() {
46
53
  return (0, nanoid_1.nanoid)().replace(/[_-]/g, '0');
47
54
  }
@@ -74,6 +81,7 @@ function identifyRedisType(redisInstance) {
74
81
  return null;
75
82
  }
76
83
  exports.identifyRedisType = identifyRedisType;
84
+ //todo: the polyfill methods will all be deleted in the `beta` release.
77
85
  exports.polyfill = {
78
86
  resolveActivityType(activityType) {
79
87
  if (activityType === 'activity') {
@@ -94,6 +102,7 @@ function identifyRedisTypeFromClass(redisClass) {
94
102
  exports.identifyRedisTypeFromClass = identifyRedisTypeFromClass;
95
103
  function matchesStatusCode(code, pattern) {
96
104
  if (typeof pattern === 'string') {
105
+ // Convert '*' wildcard to its regex equivalent (\d)
97
106
  const regexPattern = `^${pattern.replace(/\*/g, "\\d")}$`;
98
107
  return new RegExp(regexPattern).test(code.toString());
99
108
  }
@@ -105,6 +114,7 @@ function matchesStatus(status, targetStatus) {
105
114
  }
106
115
  exports.matchesStatus = matchesStatus;
107
116
  function XSleepFor(ms) {
117
+ //can be interrupted with `clearTimeout`
108
118
  let timerId;
109
119
  let promise = new Promise((resolve) => {
110
120
  timerId = setTimeout(resolve, ms);
@@ -131,6 +141,10 @@ function findSubscriptionForTrigger(obj, value) {
131
141
  return null;
132
142
  }
133
143
  exports.findSubscriptionForTrigger = findSubscriptionForTrigger;
144
+ /**
145
+ * Get the subscription topic for the flow to which @activityId belongs.
146
+ * TODO: resolve this value in the compiler...do not call this at runtime
147
+ */
134
148
  async function getSubscriptionTopic(activityId, store, appVID) {
135
149
  const appTransitions = await store.getTransitions(appVID);
136
150
  const appSubscriptions = await store.getSubscriptions(appVID);
@@ -139,6 +153,10 @@ async function getSubscriptionTopic(activityId, store, appVID) {
139
153
  return topic;
140
154
  }
141
155
  exports.getSubscriptionTopic = getSubscriptionTopic;
156
+ /**
157
+ * returns the 12-digit format of the iso timestamp (e.g, 202101010000); returns
158
+ * an empty string if overridden by the user to not segment by time (infinity).
159
+ */
142
160
  function getTimeSeries(granularity) {
143
161
  if (granularity.toString() === 'infinity') {
144
162
  return '0';
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.57",
3
+ "version": "0.0.59",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -21,6 +21,7 @@
21
21
  "start": "ts-node src/index.ts",
22
22
  "test": "NODE_ENV=test jest --detectOpenHandles --forceExit --verbose",
23
23
  "test:hmsh": "NODE_ENV=test jest ./tests/functional/index.test.ts --detectOpenHandles --verbose",
24
+ "test:unit": "NODE_ENV=test jest ./tests/unit/*/*/index.test.ts --detectOpenHandles --forceExit --verbose",
24
25
  "test:pipe": "NODE_ENV=test jest ./tests/unit/services/pipe/index.test.ts --detectOpenHandles --forceExit --verbose",
25
26
  "test:compile": "NODE_ENV=test jest ./tests/functional/compile/index.test.ts --detectOpenHandles --forceExit --verbose",
26
27
  "test:cycle": "NODE_ENV=test jest ./tests/functional/cycle/index.test.ts --detectOpenHandles --forceExit --verbose",
@@ -7,6 +7,9 @@ import { JobState, JobStatus } from '../../types/job';
7
7
  import { MultiResponseFlags, RedisClient, RedisMulti } from '../../types/redis';
8
8
  import { StringAnyType } from '../../types/serializer';
9
9
  import { StreamCode, StreamData, StreamStatus } from '../../types/stream';
10
+ /**
11
+ * The base class for all activities
12
+ */
10
13
  declare class Activity {
11
14
  config: ActivityType;
12
15
  data: ActivityData;
@@ -23,7 +26,15 @@ declare class Activity {
23
26
  adjacentIndex: number;
24
27
  constructor(config: ActivityType, data: ActivityData, metadata: ActivityMetadata, hook: ActivityData | null, engine: EngineService, context?: JobState);
25
28
  setLeg(leg: ActivityLeg): void;
29
+ /**
30
+ * Upon entering leg 1 of a duplexed activty, verify
31
+ * all aspects of the entry including job and activty state
32
+ */
26
33
  verifyEntry(): Promise<void>;
34
+ /**
35
+ * Upon entering leg 2 of a duplexed activty, verify
36
+ * all aspects of the re-entry including job and activty state
37
+ */
27
38
  verifyReentry(): Promise<number>;
28
39
  processEvent(status?: StreamStatus, code?: StreamCode, type?: 'hook' | 'output'): Promise<void>;
29
40
  processPending(telemetry: TelemetryService, type: 'hook' | 'output'): Promise<MultiResponseFlags>;
@@ -35,7 +46,14 @@ declare class Activity {
35
46
  mapInputData(): void;
36
47
  mapOutputData(): void;
37
48
  registerTimeout(): Promise<void>;
49
+ /**
50
+ * Any StreamMessage with a status of ERROR is bound to the activity
51
+ */
38
52
  bindActivityError(data: Record<string, unknown>): void;
53
+ /**
54
+ * unhandled activity errors (activities that return an ERROR StreamMessage
55
+ * status and have no adjacent children to transition to) are bound to the job
56
+ */
39
57
  bindJobError(data: Record<string, unknown>): void;
40
58
  getTriggerConfig(): Promise<ActivityType>;
41
59
  getJobStatus(): null | number;
@@ -50,6 +68,12 @@ declare class Activity {
50
68
  bindJobMetadataPaths(): string[];
51
69
  bindActivityMetadataPaths(): string[];
52
70
  getState(): Promise<void>;
71
+ /**
72
+ * if the job is created/deleted/created with the same key,
73
+ * the 'gid' ensures no stale messages (such as sleep delays)
74
+ * enter the workstream. Any message with a mismatched gid
75
+ * belongs to a prior job and can safely be ignored/dropped.
76
+ */
53
77
  assertGenerationalId(jobGID: string, msgGID?: string): void;
54
78
  initDimensionalAddress(dad: string): void;
55
79
  initSelf(context: StringAnyType): JobState;
@@ -59,6 +83,10 @@ declare class Activity {
59
83
  resolveAdjacentDad(): string;
60
84
  filterAdjacent(): Promise<StreamData[]>;
61
85
  transition(adjacencyList: StreamData[], jobStatus: JobStatus): Promise<string[]>;
86
+ /**
87
+ * A job with a vale < -100_000_000 is considered interrupted,
88
+ * as the interruption event decrements the job status by 1billion.
89
+ */
62
90
  jobWasInterrupted(jobStatus: JobStatus): boolean;
63
91
  }
64
92
  export { Activity, ActivityType };
@@ -10,6 +10,9 @@ const pipe_1 = require("../pipe");
10
10
  const serializer_1 = require("../serializer");
11
11
  const telemetry_1 = require("../telemetry");
12
12
  const stream_1 = require("../../types/stream");
13
+ /**
14
+ * The base class for all activities
15
+ */
13
16
  class Activity {
14
17
  constructor(config, data, metadata, hook, engine, context) {
15
18
  this.status = stream_1.StreamStatus.SUCCESS;
@@ -27,12 +30,20 @@ class Activity {
27
30
  setLeg(leg) {
28
31
  this.leg = leg;
29
32
  }
33
+ /**
34
+ * Upon entering leg 1 of a duplexed activty, verify
35
+ * all aspects of the entry including job and activty state
36
+ */
30
37
  async verifyEntry() {
31
38
  this.setLeg(1);
32
39
  await this.getState();
33
40
  collator_1.CollatorService.assertJobActive(this.context.metadata.js, this.context.metadata.jid, this.metadata.aid);
34
41
  await collator_1.CollatorService.notarizeEntry(this);
35
42
  }
43
+ /**
44
+ * Upon entering leg 2 of a duplexed activty, verify
45
+ * all aspects of the re-entry including job and activty state
46
+ */
36
47
  async verifyReentry() {
37
48
  const guid = this.context.metadata.guid;
38
49
  this.setLeg(2);
@@ -40,6 +51,7 @@ class Activity {
40
51
  collator_1.CollatorService.assertJobActive(this.context.metadata.js, this.context.metadata.jid, this.metadata.aid);
41
52
  return await collator_1.CollatorService.notarizeReentry(this, guid);
42
53
  }
54
+ //******** DUPLEX RE-ENTRY POINT ********//
43
55
  async processEvent(status = stream_1.StreamStatus.SUCCESS, code = 200, type = 'output') {
44
56
  this.setLeg(2);
45
57
  const jid = this.context.metadata.jid;
@@ -132,6 +144,7 @@ class Activity {
132
144
  telemetry.mapActivityAttributes();
133
145
  const jobStatus = this.resolveStatus(multiResponse);
134
146
  const attrs = { 'app.job.jss': jobStatus };
147
+ //adjacencyList membership has already been set at this point (according to activity status)
135
148
  const messageIds = await this.transition(this.adjacencyList, jobStatus);
136
149
  if (messageIds?.length) {
137
150
  attrs['app.activity.mids'] = messageIds.join(',');
@@ -154,6 +167,10 @@ class Activity {
154
167
  if (output) {
155
168
  for (const key in output) {
156
169
  const f1 = key.indexOf('[');
170
+ //keys with array notation suffix `somekey[]` represent
171
+ //dynamically-keyed mappings whose `value` must be moved to the output.
172
+ //The `value` must be an object with keys appropriate to the
173
+ //notation type: `somekey[0] (array)`, `somekey[-] (mark)`, OR `somekey[_] (search)`
157
174
  if (f1 > -1) {
158
175
  const amount = key.substring(f1 + 1).split(']')[0];
159
176
  if (!isNaN(Number(amount))) {
@@ -180,6 +197,7 @@ class Activity {
180
197
  }
181
198
  }
182
199
  mapOutputData() {
200
+ //activity YAML may include output map data that produces/extends activity output data.
183
201
  if (this.config.output?.maps) {
184
202
  const mapper = new mapper_1.MapperService(this.config.output.maps, this.context);
185
203
  const actOutData = mapper.mapRules();
@@ -189,12 +207,21 @@ class Activity {
189
207
  }
190
208
  }
191
209
  async registerTimeout() {
210
+ //set timeout in support of hook and/or duplex
192
211
  }
212
+ /**
213
+ * Any StreamMessage with a status of ERROR is bound to the activity
214
+ */
193
215
  bindActivityError(data) {
194
216
  const md = this.context[this.metadata.aid].output.metadata;
195
217
  md.err = JSON.stringify(this.data);
218
+ //(temporary...useful for mapping error parts in the app.yaml)
196
219
  md.$error = { ...data, is_stream_error: true };
197
220
  }
221
+ /**
222
+ * unhandled activity errors (activities that return an ERROR StreamMessage
223
+ * status and have no adjacent children to transition to) are bound to the job
224
+ */
198
225
  bindJobError(data) {
199
226
  this.context.metadata.err = JSON.stringify({ ...data, is_stream_error: true });
200
227
  }
@@ -209,6 +236,7 @@ class Activity {
209
236
  await this.store.setStatus(amount, this.context.metadata.jid, appId, multi);
210
237
  }
211
238
  authorizeEntry(state) {
239
+ //pre-authorize activity state to allow entry for adjacent activities
212
240
  return this.adjacencyList?.map((streamData) => {
213
241
  const { metadata: { aid } } = streamData;
214
242
  state[`${aid}/output/metadata/as`] = collator_1.CollatorService.getSeed();
@@ -228,6 +256,7 @@ class Activity {
228
256
  const presets = this.authorizeEntry(state);
229
257
  this.bindDimensionalAddress(state);
230
258
  this.bindActivityState(state);
259
+ //symbolNames holds symkeys
231
260
  const symbolNames = [
232
261
  `$${this.config.subscribes}`,
233
262
  this.metadata.aid,
@@ -237,6 +266,7 @@ class Activity {
237
266
  return await this.store.setState(state, this.getJobStatus(), jobId, symbolNames, dIds, multi);
238
267
  }
239
268
  bindJobMetadata() {
269
+ //both legs of the most recently run activity (1 and 2) modify ju (job_updated)
240
270
  this.context.metadata.ju = (0, utils_1.formatISODate)(new Date());
241
271
  }
242
272
  bindActivityMetadata() {
@@ -323,6 +353,7 @@ class Activity {
323
353
  telemetry_1.TelemetryService.addTargetTelemetryPaths(consumes, this.config, this.metadata, this.leg);
324
354
  let { dad, jid } = this.context.metadata;
325
355
  const dIds = collator_1.CollatorService.getDimensionsById([...this.config.ancestors, this.metadata.aid], dad || '');
356
+ //`state` is a unidimensional hash; context is a tree
326
357
  const [state, _status] = await this.store.getState(jid, consumes, dIds);
327
358
  this.context = (0, utils_1.restoreHierarchy)(state);
328
359
  this.assertGenerationalId(this.context?.metadata?.gid, gid);
@@ -330,6 +361,12 @@ class Activity {
330
361
  this.initSelf(this.context);
331
362
  this.initPolicies(this.context);
332
363
  }
364
+ /**
365
+ * if the job is created/deleted/created with the same key,
366
+ * the 'gid' ensures no stale messages (such as sleep delays)
367
+ * enter the workstream. Any message with a mismatched gid
368
+ * belongs to a prior job and can safely be ignored/dropped.
369
+ */
333
370
  assertGenerationalId(jobGID, msgGID) {
334
371
  if (msgGID !== jobGID) {
335
372
  throw new errors_1.GenerationalError(jobGID, msgGID, this.context?.metadata?.jid ?? '', this.context?.metadata?.aid ?? '', this.context?.metadata?.dad ?? '');
@@ -356,9 +393,10 @@ class Activity {
356
393
  if (!self.output.metadata) {
357
394
  self.output.metadata = {};
358
395
  }
396
+ //prebind the updated timestamp (mappings need the time)
359
397
  self.output.metadata.au = (0, utils_1.formatISODate)(new Date());
360
398
  context['$self'] = self;
361
- context['$job'] = context;
399
+ context['$job'] = context; //NEVER call STRINGIFY! (now circular)
362
400
  return context;
363
401
  }
364
402
  initPolicies(context) {
@@ -371,11 +409,13 @@ class Activity {
371
409
  resolveDad() {
372
410
  let dad = this.metadata.dad;
373
411
  if (this.adjacentIndex > 0) {
412
+ //if adjacent index > 0 the activity is cycling; replace last index with cycle index
374
413
  dad = `${dad.substring(0, dad.lastIndexOf(','))},${this.adjacentIndex}`;
375
414
  }
376
415
  return dad;
377
416
  }
378
417
  resolveAdjacentDad() {
418
+ //concat self and child dimension (all children (leg 1) begin life at 0)
379
419
  return `${this.resolveDad()}${collator_1.CollatorService.getDimensionalSeed(0)}`;
380
420
  }
381
421
  ;
@@ -383,6 +423,7 @@ class Activity {
383
423
  const adjacencyList = [];
384
424
  const transitions = await this.store.getTransitions(await this.engine.getVID());
385
425
  const transition = transitions[`.${this.metadata.aid}`];
426
+ //resolve the dimensional address for adjacent children
386
427
  const adjacentDad = this.resolveAdjacentDad();
387
428
  if (transition) {
388
429
  for (const toActivityId in transition) {
@@ -427,6 +468,10 @@ class Activity {
427
468
  }
428
469
  return mIds;
429
470
  }
471
+ /**
472
+ * A job with a vale < -100_000_000 is considered interrupted,
473
+ * as the interruption event decrements the job status by 1billion.
474
+ */
430
475
  jobWasInterrupted(jobStatus) {
431
476
  return jobStatus < -100000000;
432
477
  }
@@ -12,6 +12,7 @@ class Await extends activity_1.Activity {
12
12
  constructor(config, data, metadata, hook, engine, context) {
13
13
  super(config, data, metadata, hook, engine, context);
14
14
  }
15
+ //******** INITIAL ENTRY POINT (A) ********//
15
16
  async process() {
16
17
  this.logger.debug('await-process', { jid: this.context.metadata.jid, gid: this.context.metadata.gid, aid: this.metadata.aid });
17
18
  let telemetry;
@@ -20,12 +21,15 @@ class Await extends activity_1.Activity {
20
21
  telemetry = new telemetry_1.TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
21
22
  telemetry.startActivitySpan(this.leg);
22
23
  this.mapInputData();
24
+ //save state and authorize reentry
23
25
  const multi = this.store.getMulti();
26
+ //todo: await this.registerTimeout();
24
27
  const messageId = await this.execActivity(multi);
25
28
  await collator_1.CollatorService.authorizeReentry(this, multi);
26
29
  await this.setState(multi);
27
30
  await this.setStatus(0, multi);
28
31
  const multiResponse = await multi.exec();
32
+ //telemetry
29
33
  telemetry.mapActivityAttributes();
30
34
  const jobStatus = this.resolveStatus(multiResponse);
31
35
  telemetry.setActivityAttributes({
@@ -7,6 +7,13 @@ declare class Cycle extends Activity {
7
7
  config: CycleActivity;
8
8
  constructor(config: ActivityType, data: ActivityData, metadata: ActivityMetadata, hook: ActivityData | null, engine: EngineService, context?: JobState);
9
9
  process(): Promise<string>;
10
+ /**
11
+ * Trigger the target ancestor to execute in a cycle,
12
+ * without violating the constraints of the DAG. Immutable
13
+ * `individual activity state` will execute in a new dimensional
14
+ * thread while `shared job state` can change. This
15
+ * pattern allows for retries without violating the DAG.
16
+ */
10
17
  cycleAncestorActivity(multi: RedisMulti): Promise<string>;
11
18
  }
12
19
  export { Cycle };
@@ -10,6 +10,7 @@ class Cycle extends activity_1.Activity {
10
10
  constructor(config, data, metadata, hook, engine, context) {
11
11
  super(config, data, metadata, hook, engine, context);
12
12
  }
13
+ //******** LEG 1 ENTRY ********//
13
14
  async process() {
14
15
  this.logger.debug('cycle-process', { jid: this.context.metadata.jid, gid: this.context.metadata.gid, aid: this.metadata.aid });
15
16
  let telemetry;
@@ -18,18 +19,21 @@ class Cycle extends activity_1.Activity {
18
19
  telemetry = new telemetry_1.TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
19
20
  telemetry.startActivitySpan(this.leg);
20
21
  this.mapInputData();
22
+ //set state/status
21
23
  let multi = this.store.getMulti();
22
24
  await this.setState(multi);
23
- await this.setStatus(0, multi);
25
+ await this.setStatus(0, multi); //leg 1 never changes job status
24
26
  const multiResponse = await multi.exec();
25
27
  telemetry.mapActivityAttributes();
26
28
  const jobStatus = this.resolveStatus(multiResponse);
29
+ //cycle the target ancestor
27
30
  multi = this.store.getMulti();
28
31
  const messageId = await this.cycleAncestorActivity(multi);
29
32
  telemetry.setActivityAttributes({
30
33
  'app.activity.mid': messageId,
31
34
  'app.job.jss': jobStatus
32
35
  });
36
+ //exit early (`Cycle` activities only execute Leg 1)
33
37
  await collator_1.CollatorService.notarizeEarlyExit(this, multi);
34
38
  await multi.exec();
35
39
  return this.context.metadata.aid;
@@ -58,7 +62,18 @@ class Cycle extends activity_1.Activity {
58
62
  this.logger.debug('cycle-process-end', { jid: this.context.metadata.jid, gid: this.context.metadata.gid, aid: this.metadata.aid });
59
63
  }
60
64
  }
65
+ /**
66
+ * Trigger the target ancestor to execute in a cycle,
67
+ * without violating the constraints of the DAG. Immutable
68
+ * `individual activity state` will execute in a new dimensional
69
+ * thread while `shared job state` can change. This
70
+ * pattern allows for retries without violating the DAG.
71
+ */
61
72
  async cycleAncestorActivity(multi) {
73
+ //Cycle activity L1 is a standin for the target ancestor L1.
74
+ //Input data mapping (mapInputData) allows for the
75
+ //next dimensonal thread to execute with different
76
+ //input data than the current dimensional thread
62
77
  this.mapInputData();
63
78
  const streamData = {
64
79
  metadata: {
@@ -6,10 +6,16 @@ import { HookRule } from '../../types/hook';
6
6
  import { JobState, JobStatus } from '../../types/job';
7
7
  import { RedisMulti } from '../../types/redis';
8
8
  import { StreamCode, StreamStatus } from '../../types/stream';
9
+ /**
10
+ * Supports `signal hook`, `time hook`, and `cycle hook` patterns
11
+ */
9
12
  declare class Hook extends Activity {
10
13
  config: HookActivity;
11
14
  constructor(config: ActivityType, data: ActivityData, metadata: ActivityMetadata, hook: ActivityData | null, engine: EngineService, context?: JobState);
12
15
  process(): Promise<string>;
16
+ /**
17
+ * does this activity use a time-hook or web-hook
18
+ */
13
19
  doesHook(): boolean;
14
20
  doHook(telemetry: TelemetryService): Promise<void>;
15
21
  doPassThrough(telemetry: TelemetryService): Promise<void>;