@hotmeshio/hotmesh 0.0.48 → 0.0.50

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 (66) hide show
  1. package/README.md +1 -1
  2. package/build/modules/enums.d.ts +1 -0
  3. package/build/modules/enums.js +2 -1
  4. package/build/modules/key.d.ts +5 -1
  5. package/build/modules/key.js +10 -2
  6. package/build/package.json +2 -1
  7. package/build/services/activities/await.js +6 -0
  8. package/build/services/activities/hook.js +1 -1
  9. package/build/services/activities/trigger.d.ts +1 -0
  10. package/build/services/activities/trigger.js +23 -2
  11. package/build/services/durable/exporter.js +19 -5
  12. package/build/services/durable/meshos.js +11 -6
  13. package/build/services/durable/search.d.ts +20 -1
  14. package/build/services/durable/search.js +73 -25
  15. package/build/services/durable/worker.js +10 -0
  16. package/build/services/durable/workflow.d.ts +1 -0
  17. package/build/services/durable/workflow.js +17 -1
  18. package/build/services/engine/index.d.ts +1 -1
  19. package/build/services/engine/index.js +12 -3
  20. package/build/services/exporter/index.js +3 -2
  21. package/build/services/hotmesh/index.js +4 -0
  22. package/build/services/quorum/index.d.ts +11 -2
  23. package/build/services/quorum/index.js +33 -0
  24. package/build/services/router/index.d.ts +15 -0
  25. package/build/services/router/index.js +55 -7
  26. package/build/services/serializer/index.js +1 -1
  27. package/build/services/store/clients/redis.js +2 -0
  28. package/build/services/store/index.d.ts +6 -4
  29. package/build/services/store/index.js +86 -21
  30. package/build/services/task/index.d.ts +2 -1
  31. package/build/services/task/index.js +30 -13
  32. package/build/services/worker/index.d.ts +13 -2
  33. package/build/services/worker/index.js +44 -3
  34. package/build/types/activity.d.ts +1 -0
  35. package/build/types/durable.d.ts +9 -0
  36. package/build/types/exporter.d.ts +2 -0
  37. package/build/types/job.d.ts +1 -0
  38. package/build/types/quorum.d.ts +22 -8
  39. package/build/types/stream.d.ts +1 -0
  40. package/modules/enums.ts +1 -0
  41. package/modules/key.ts +7 -2
  42. package/package.json +2 -1
  43. package/services/activities/await.ts +6 -0
  44. package/services/activities/hook.ts +1 -0
  45. package/services/activities/trigger.ts +25 -1
  46. package/services/durable/exporter.ts +18 -7
  47. package/services/durable/meshos.ts +10 -6
  48. package/services/durable/search.ts +73 -26
  49. package/services/durable/worker.ts +13 -1
  50. package/services/durable/workflow.ts +18 -0
  51. package/services/engine/index.ts +13 -5
  52. package/services/exporter/index.ts +3 -2
  53. package/services/hotmesh/index.ts +4 -0
  54. package/services/quorum/index.ts +38 -2
  55. package/services/router/index.ts +59 -9
  56. package/services/serializer/index.ts +1 -1
  57. package/services/store/clients/redis.ts +2 -0
  58. package/services/store/index.ts +108 -22
  59. package/services/task/index.ts +31 -11
  60. package/services/worker/index.ts +49 -5
  61. package/types/activity.ts +1 -0
  62. package/types/durable.ts +11 -0
  63. package/types/exporter.ts +2 -0
  64. package/types/job.ts +1 -0
  65. package/types/quorum.ts +28 -13
  66. package/types/stream.ts +1 -0
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 elevates Redis from an in-memory data cache to a distributed orchestration engine.
4
+ HotMesh transforms Redis into a distributed orchestration engine.
5
5
 
6
6
  *Write functions in your own preferred style, and let Redis govern their execution, reliably and durably.*
7
7
 
@@ -15,6 +15,7 @@ export declare const HMSH_CODE_DURABLE_MAXED = 597;
15
15
  export declare const HMSH_CODE_DURABLE_FATAL = 598;
16
16
  export declare const HMSH_CODE_DURABLE_RETRYABLE = 599;
17
17
  export declare const HMSH_STATUS_UNKNOWN = "unknown";
18
+ export declare const HMSH_QUORUM_ROLLCALL_CYCLES = 12;
18
19
  export declare const HMSH_QUORUM_DELAY_MS = 250;
19
20
  export declare const HMSH_ACTIVATION_MAX_RETRY = 3;
20
21
  export declare const HMSH_OTT_WAIT_TIME: number;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
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_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_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_WAITFOR = exports.HMSH_CODE_DURABLE_INCOMPLETE = exports.HMSH_CODE_DURABLE_SLEEPFOR = 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;
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_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_WAITFOR = exports.HMSH_CODE_DURABLE_INCOMPLETE = exports.HMSH_CODE_DURABLE_SLEEPFOR = 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
4
  // HOTMESH SYSTEM
5
5
  exports.HMSH_LOGLEVEL = process.env.HMSH_LOGLEVEL || 'info';
6
6
  // STATUS CODES AND MESSAGES
@@ -20,6 +20,7 @@ exports.HMSH_CODE_DURABLE_FATAL = 598;
20
20
  exports.HMSH_CODE_DURABLE_RETRYABLE = 599;
21
21
  exports.HMSH_STATUS_UNKNOWN = 'unknown';
22
22
  // QUORUM
23
+ exports.HMSH_QUORUM_ROLLCALL_CYCLES = 12; //max iterations
23
24
  exports.HMSH_QUORUM_DELAY_MS = 250;
24
25
  exports.HMSH_ACTIVATION_MAX_RETRY = 3;
25
26
  // ENGINE
@@ -27,6 +27,10 @@ import { KeyStoreParams, KeyType } from '../types/hotmesh';
27
27
  * hmsh:<appid>:sym:vals: -> {hash} list of symbols for job values across all app versions
28
28
  */
29
29
  declare const HMNS = "hmsh";
30
+ declare const KEYSEP = ":";
31
+ declare const VALSEP = "::";
32
+ declare const WEBSEP = "::";
33
+ declare const TYPSEP = "::";
30
34
  declare class KeyService {
31
35
  /**
32
36
  * returns a key that can be used to access a value in the key/value store
@@ -41,4 +45,4 @@ declare class KeyService {
41
45
  */
42
46
  static mintKey(namespace: string, keyType: KeyType, params: KeyStoreParams): string;
43
47
  }
44
- export { KeyService, KeyType, KeyStoreParams, HMNS };
48
+ export { KeyService, KeyType, KeyStoreParams, HMNS, KEYSEP, TYPSEP, WEBSEP, VALSEP };
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HMNS = exports.KeyType = exports.KeyService = void 0;
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
6
  /**
@@ -30,8 +30,16 @@ Object.defineProperty(exports, "KeyType", { enumerable: true, get: function () {
30
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
31
  * hmsh:<appid>:sym:vals: -> {hash} list of symbols for job values across all app versions
32
32
  */
33
- const HMNS = "hmsh"; //default
33
+ const HMNS = "hmsh";
34
34
  exports.HMNS = HMNS;
35
+ const KEYSEP = ':'; //default delimiter for keys
36
+ exports.KEYSEP = KEYSEP;
37
+ const VALSEP = '::'; //default delimiter for vals
38
+ exports.VALSEP = VALSEP;
39
+ const WEBSEP = '::'; //default delimiter for webhook vals
40
+ exports.WEBSEP = WEBSEP;
41
+ const TYPSEP = '::'; //delimiter for ZSET task typing (how should a list be used?)
42
+ exports.TYPSEP = TYPSEP;
35
43
  class KeyService {
36
44
  /**
37
45
  * returns a key that can be used to access a value in the key/value store
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.48",
3
+ "version": "0.0.50",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -28,6 +28,7 @@
28
28
  "test:connect:redis": "NODE_ENV=test jest ./tests/unit/services/connector/clients/redis.test.ts --detectOpenHandles --forceExit --verbose",
29
29
  "test:connect:ioredis": "NODE_ENV=test jest ./tests/unit/services/connector/clients/ioredis.test.ts --detectOpenHandles --forceExit --verbose",
30
30
  "test:emit": "NODE_ENV=test jest ./tests/functional/emit/index.test.ts --detectOpenHandles --forceExit --verbose",
31
+ "test:await": "NODE_ENV=test jest ./tests/functional/awaiter/index.test.ts --detectOpenHandles --forceExit --verbose",
31
32
  "test:hook": "NODE_ENV=test jest ./tests/functional/hook/index.test.ts --detectOpenHandles --forceExit --verbose",
32
33
  "test:signal": "NODE_ENV=test jest ./tests/functional/signal/index.test.ts --detectOpenHandles --forceExit --verbose",
33
34
  "test:interrupt": "NODE_ENV=test jest ./tests/functional/interrupt/index.test.ts --detectOpenHandles --forceExit --verbose",
@@ -78,6 +78,12 @@ class Await extends activity_1.Activity {
78
78
  type: stream_1.StreamDataType.AWAIT,
79
79
  data: this.context.data
80
80
  };
81
+ if (this.config.await !== true) {
82
+ const doAwait = pipe_1.Pipe.resolve(this.config.await, this.context);
83
+ if (doAwait === false) {
84
+ streamData.metadata.await = false;
85
+ }
86
+ }
81
87
  if (this.config.retry) {
82
88
  streamData.policies = {
83
89
  retry: this.config.retry
@@ -107,7 +107,7 @@ class Hook extends activity_1.Activity {
107
107
  }
108
108
  else if (this.config.sleep) {
109
109
  const duration = pipe_1.Pipe.resolve(this.config.sleep, this.context);
110
- await this.engine.taskService.registerTimeHook(this.context.metadata.jid, this.context.metadata.gid, `${this.metadata.aid}${this.metadata.dad || ''}`, 'sleep', duration);
110
+ await this.engine.taskService.registerTimeHook(this.context.metadata.jid, this.context.metadata.gid, `${this.metadata.aid}${this.metadata.dad || ''}`, 'sleep', duration, this.metadata.dad || '');
111
111
  return this.context.metadata.jid;
112
112
  }
113
113
  }
@@ -8,6 +8,7 @@ declare class Trigger extends Activity {
8
8
  constructor(config: ActivityType, data: ActivityData, metadata: ActivityMetadata, hook: ActivityData | null, engine: EngineService, context?: JobState);
9
9
  process(): Promise<string>;
10
10
  setStatus(amount: number): Promise<void>;
11
+ execAdjacentParent(): Promise<void>;
11
12
  createInputContext(): Partial<JobState>;
12
13
  getState(): Promise<void>;
13
14
  bindJobMetadataPaths(): string[];
@@ -31,6 +31,9 @@ class Trigger extends activity_1.Activity {
31
31
  await this.setStats(multi);
32
32
  await this.registerJobDependency(multi);
33
33
  await multi.exec();
34
+ //if the parent (spawner) chose not to await,
35
+ // emit the job_id as the data payload { job_id }
36
+ this.execAdjacentParent();
34
37
  telemetry.mapActivityAttributes();
35
38
  const jobStatus = Number(this.context.metadata.js);
36
39
  telemetry.setJobAttributes({ 'app.job.jss': jobStatus });
@@ -61,6 +64,11 @@ class Trigger extends activity_1.Activity {
61
64
  async setStatus(amount) {
62
65
  this.context.metadata.js = amount;
63
66
  }
67
+ async execAdjacentParent() {
68
+ if (this.context.metadata.px) {
69
+ await this.engine.execAdjacentParent(this.context, { metadata: this.context.metadata, data: { job_id: this.context.metadata.jid } });
70
+ }
71
+ }
64
72
  createInputContext() {
65
73
  const input = {
66
74
  [this.metadata.aid]: {
@@ -95,6 +103,7 @@ class Trigger extends activity_1.Activity {
95
103
  pg: this.context.metadata.pg,
96
104
  pd: this.context.metadata.pd,
97
105
  pa: this.context.metadata.pa,
106
+ px: this.context.metadata.px,
98
107
  app: id,
99
108
  vrs: version,
100
109
  tpc: this.config.subscribes,
@@ -165,10 +174,22 @@ class Trigger extends activity_1.Activity {
165
174
  }
166
175
  if (resolvedDepKey) {
167
176
  const isParentOrigin = (resolvedDepKey === this.context.metadata.pj) || (resolvedDepKey === resolvedAdjKey);
168
- await this.store.registerJobDependency(isParentOrigin ? 'expire-child' : 'expire', resolvedDepKey, this.context.metadata.tpc, this.context.metadata.jid, this.context.metadata.gid, multi);
177
+ let type;
178
+ if (isParentOrigin) {
179
+ if (this.context.metadata.px) {
180
+ type = 'child';
181
+ }
182
+ else {
183
+ type = 'expire-child';
184
+ }
185
+ }
186
+ else {
187
+ type = 'expire';
188
+ }
189
+ await this.store.registerJobDependency(type, resolvedDepKey, this.context.metadata.tpc, this.context.metadata.jid, this.context.metadata.gid, this.context.metadata.pd, multi);
169
190
  }
170
191
  if (resolvedAdjKey && resolvedAdjKey !== resolvedDepKey) {
171
- await this.store.registerJobDependency('child', resolvedAdjKey, this.context.metadata.tpc, this.context.metadata.jid, this.context.metadata.gid, multi);
192
+ await this.store.registerJobDependency('child', resolvedAdjKey, this.context.metadata.tpc, this.context.metadata.jid, this.context.metadata.gid, this.context.metadata.pd, multi);
172
193
  }
173
194
  }
174
195
  async setStats(multi) {
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ExporterService = void 0;
4
4
  const serializer_1 = require("../serializer");
5
5
  const utils_1 = require("../../modules/utils");
6
+ const key_1 = require("../../modules/key");
6
7
  /**
7
8
  * Downloads job data from Redis (hscan, hmget, hgetall)
8
9
  * Splits, Inflates, and Sorts the job data for use in durable contexts
@@ -94,13 +95,27 @@ class ExporterService {
94
95
  const activityName = item[1].split('/')[0];
95
96
  const duplex = item[1].endsWith('/ac') ? 'entry' : 'exit';
96
97
  const timestamp = item[2];
97
- const event = {
98
+ let event = {
98
99
  activity: activityName,
99
100
  duplex: duplex,
100
101
  dimension: dimensions,
101
102
  timestamp,
103
+ created: timestamp,
104
+ updated: timestamp,
102
105
  };
103
- timeline.push(event);
106
+ const prior = timeline[timeline.length - 1];
107
+ if (prior && prior.activity === event.activity && prior.duplex !== event.duplex && prior.dimension === event.dimension) {
108
+ if (event.duplex === 'exit') {
109
+ prior.updated = event.timestamp;
110
+ }
111
+ else {
112
+ prior.created = event.timestamp;
113
+ }
114
+ event = prior;
115
+ }
116
+ else {
117
+ timeline.push(event);
118
+ }
104
119
  if (this.isMainEntry(item[1])) {
105
120
  event.actions = [];
106
121
  this.interleaveActions(actions.main, event.actions);
@@ -163,12 +178,11 @@ class ExporterService {
163
178
  * @returns - the organized dependency data
164
179
  */
165
180
  inflateDependencyData(data, actions) {
166
- //console.log('dependency data>', data);
167
181
  const hookReg = /([0-9,]+)-(\d+)$/;
168
182
  const flowReg = /-(\d+)$/;
169
183
  return data.map((dependency, index) => {
170
- const [action, topic, gid, ...jid] = dependency.split('::');
171
- const jobId = jid.join('::');
184
+ const [action, topic, gid, _pd, ...jid] = dependency.split(key_1.VALSEP);
185
+ const jobId = jid.join(key_1.VALSEP);
172
186
  const match = jobId.match(hookReg);
173
187
  let prefix;
174
188
  let type;
@@ -187,13 +187,18 @@ class MeshOSService {
187
187
  }
188
188
  else {
189
189
  //limit which hash fields to return
190
- if (options.return?.length) {
191
- args.push('RETURN');
192
- args.push(options.return.length.toString());
193
- options.return.forEach(returnField => {
190
+ args.push('RETURN');
191
+ args.push(((options.return?.length ?? 0) + 1).toString());
192
+ args.push('$');
193
+ options.return?.forEach(returnField => {
194
+ if (returnField.startsWith('"')) {
195
+ //allow literal values to be requested
196
+ args.push(returnField.slice(1, -1));
197
+ }
198
+ else {
194
199
  args.push(`_${returnField}`);
195
- });
196
- }
200
+ }
201
+ });
197
202
  //paginate
198
203
  if (options.limit) {
199
204
  args.push('LIMIT', options.limit.start.toString(), options.limit.size.toString());
@@ -30,10 +30,29 @@ export declare class Search {
30
30
  * calling any method that produces side effects (changes the value)
31
31
  */
32
32
  getSearchSessionGuid(): string;
33
- set(...args: string[]): Promise<void>;
33
+ /**
34
+ * Sets the fields listed in args. Returns the
35
+ * count of new fields that were set (does not
36
+ * count fields that were updated)
37
+ */
38
+ set(...args: string[]): Promise<number>;
34
39
  get(key: string): Promise<string>;
35
40
  mget(...args: string[]): Promise<string[]>;
41
+ /**
42
+ * Deletes the fields listed in args. Returns the
43
+ * count of fields that were deleted.
44
+ */
36
45
  del(...args: string[]): Promise<number | void>;
46
+ /**
47
+ * Increments the value of a field by the given amount. Returns the
48
+ * new value of the field after the increment. Can be
49
+ * used to decrement the value of a field by specifying a negative.
50
+ */
37
51
  incr(key: string, val: number): Promise<number>;
52
+ /**
53
+ * Multiplies the value of a field by the given amount. Returns the
54
+ * new value of the field after the multiplication. NOTE:
55
+ * this is exponential multiplication.
56
+ */
38
57
  mult(key: string, val: number): Promise<number>;
39
58
  }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Search = void 0;
4
4
  const key_1 = require("../../modules/key");
5
+ const storage_1 = require("../../modules/storage");
5
6
  class Search {
6
7
  constructor(workflowId, hotMeshClient, searchSessionId) {
7
8
  this.searchSessionIndex = 0;
@@ -57,9 +58,15 @@ class Search {
57
58
  * @returns {Promise<string[]>} - the list of search indexes
58
59
  */
59
60
  static async listSearchIndexes(hotMeshClient) {
60
- const store = hotMeshClient.engine.store;
61
- const searchIndexes = await store.exec('FT._LIST');
62
- return searchIndexes;
61
+ try {
62
+ const store = hotMeshClient.engine.store;
63
+ const searchIndexes = await store.exec('FT._LIST');
64
+ return searchIndexes;
65
+ }
66
+ catch (err) {
67
+ hotMeshClient.engine.logger.info('durable-client-search-list-err', { err });
68
+ return [];
69
+ }
63
70
  }
64
71
  /**
65
72
  * increments the index to return a unique search session guid when
@@ -69,18 +76,28 @@ class Search {
69
76
  //return the search session as it would exist in the search session index
70
77
  return `${this.searchSessionId}-${this.searchSessionIndex++}-`;
71
78
  }
79
+ /**
80
+ * Sets the fields listed in args. Returns the
81
+ * count of new fields that were set (does not
82
+ * count fields that were updated)
83
+ */
72
84
  async set(...args) {
73
85
  const ssGuid = this.getSearchSessionGuid();
74
- const ssGuidValue = Number(await this.store.exec('HINCRBYFLOAT', this.jobId, ssGuid, '1'));
75
- if (ssGuidValue === 1) {
76
- const safeArgs = [];
77
- for (let i = 0; i < args.length; i += 2) {
78
- const key = this.safeKey(args[i]);
79
- const value = args[i + 1].toString();
80
- safeArgs.push(key, value);
81
- }
82
- await this.store.exec('HSET', this.jobId, ...safeArgs);
86
+ const store = storage_1.asyncLocalStorage.getStore();
87
+ const replay = store?.get('replay') ?? {};
88
+ if (ssGuid in replay) {
89
+ return Number(replay[ssGuid]);
83
90
  }
91
+ const safeArgs = [];
92
+ for (let i = 0; i < args.length; i += 2) {
93
+ const key = this.safeKey(args[i]);
94
+ const value = args[i + 1].toString();
95
+ safeArgs.push(key, value);
96
+ }
97
+ const fieldCount = await this.store.exec('HSET', this.jobId, ...safeArgs);
98
+ //no need to wait; set this interim value in the replay
99
+ this.store.exec('HSET', this.jobId, ssGuid, fieldCount.toString());
100
+ return Number(fieldCount);
84
101
  }
85
102
  async get(key) {
86
103
  try {
@@ -104,32 +121,63 @@ class Search {
104
121
  return [];
105
122
  }
106
123
  }
124
+ /**
125
+ * Deletes the fields listed in args. Returns the
126
+ * count of fields that were deleted.
127
+ */
107
128
  async del(...args) {
108
129
  const ssGuid = this.getSearchSessionGuid();
109
- const ssGuidValue = Number(await this.store.exec('HINCRBYFLOAT', this.jobId, ssGuid, '1'));
110
- if (ssGuidValue === 1) {
111
- const safeArgs = [];
112
- for (let i = 0; i < args.length; i++) {
113
- safeArgs.push(this.safeKey(args[i]));
114
- }
115
- const response = await this.store.exec('HDEL', this.jobId, ...safeArgs);
116
- return isNaN(response) ? undefined : Number(response);
130
+ const store = storage_1.asyncLocalStorage.getStore();
131
+ const replay = store?.get('replay') ?? {};
132
+ if (ssGuid in replay) {
133
+ return Number(replay[ssGuid]);
134
+ }
135
+ const safeArgs = [];
136
+ for (let i = 0; i < args.length; i++) {
137
+ safeArgs.push(this.safeKey(args[i]));
117
138
  }
139
+ const response = await this.store.exec('HDEL', this.jobId, ...safeArgs);
140
+ const formattedResponse = isNaN(response) ? 0 : Number(response);
141
+ //no need to wait; set this interim value in the replay
142
+ this.store.exec('HSET', this.jobId, ssGuid, formattedResponse.toString());
143
+ return formattedResponse;
118
144
  }
145
+ /**
146
+ * Increments the value of a field by the given amount. Returns the
147
+ * new value of the field after the increment. Can be
148
+ * used to decrement the value of a field by specifying a negative.
149
+ */
119
150
  async incr(key, val) {
120
151
  const ssGuid = this.getSearchSessionGuid();
121
- const ssGuidValue = Number(await this.store.exec('HINCRBYFLOAT', this.jobId, ssGuid, '1'));
122
- if (ssGuidValue === 1) {
123
- return Number(await this.store.exec('HINCRBYFLOAT', this.jobId, this.safeKey(key), val.toString()));
152
+ const store = storage_1.asyncLocalStorage.getStore();
153
+ const replay = store?.get('replay') ?? {};
154
+ if (ssGuid in replay) {
155
+ return Number(replay[ssGuid]);
124
156
  }
157
+ const num = await this.store.exec('HINCRBYFLOAT', this.jobId, this.safeKey(key), val.toString());
158
+ //no need to wait; set this interim value in the replay
159
+ this.store.exec('HSET', this.jobId, ssGuid, num.toString());
160
+ return Number(num);
125
161
  }
162
+ /**
163
+ * Multiplies the value of a field by the given amount. Returns the
164
+ * new value of the field after the multiplication. NOTE:
165
+ * this is exponential multiplication.
166
+ */
126
167
  async mult(key, val) {
127
168
  const ssGuid = this.getSearchSessionGuid();
169
+ const store = storage_1.asyncLocalStorage.getStore();
170
+ const replay = store?.get('replay') ?? {};
171
+ if (ssGuid in replay) {
172
+ return Math.exp(Number(replay[ssGuid]));
173
+ }
128
174
  const ssGuidValue = Number(await this.store.exec('HINCRBYFLOAT', this.jobId, ssGuid, '1'));
129
175
  if (ssGuidValue === 1) {
130
176
  const log = Math.log(val);
131
- const logTotal = Number(await this.store.exec('HINCRBYFLOAT', this.jobId, this.safeKey(key), log.toString()));
132
- return Math.exp(logTotal);
177
+ const logTotal = await this.store.exec('HINCRBYFLOAT', this.jobId, this.safeKey(key), log.toString());
178
+ //no need to wait; set this interim value in the replay
179
+ this.store.exec('HSET', this.jobId, ssGuid, logTotal.toString());
180
+ return Math.exp(Number(logTotal));
133
181
  }
134
182
  }
135
183
  }
@@ -164,16 +164,26 @@ class WorkerService {
164
164
  // garbage collect (expire) this job when originJobId is expired
165
165
  context.set('originJobId', workflowInput.originJobId);
166
166
  }
167
+ let replayQuery = '';
167
168
  if (workflowInput.workflowDimension) {
168
169
  //every hook function runs in an isolated dimension controlled
169
170
  //by the index assigned when the signal was received; even if the
170
171
  //hook function re-runs, its scope will always remain constant
171
172
  context.set('workflowDimension', workflowInput.workflowDimension);
173
+ replayQuery = `-*${workflowInput.workflowDimension}-*`;
174
+ }
175
+ else {
176
+ //last letter of words like 'hook', 'sleep', 'wait', 'signal', 'search', 'start'
177
+ replayQuery = '-*[ehklpt]-*';
172
178
  }
173
179
  context.set('workflowTopic', workflowTopic);
174
180
  context.set('workflowName', workflowTopic.split('-').pop());
175
181
  context.set('workflowTrace', data.metadata.trc);
176
182
  context.set('workflowSpan', data.metadata.spn);
183
+ const store = this.workflowRunner.engine.store;
184
+ const [cursor, replay] = await store.findJobFields(workflowInput.workflowId, replayQuery, 50000, 5000);
185
+ context.set('replay', replay);
186
+ context.set('cursor', cursor); // if != 0, more remain
177
187
  const workflowResponse = await storage_1.asyncLocalStorage.run(context, async () => {
178
188
  return await workflowFunction.apply(this, workflowInput.arguments);
179
189
  });
@@ -87,6 +87,7 @@ export declare class WorkflowService {
87
87
  workflowTopic: any;
88
88
  workflowDimension: any;
89
89
  counter: any;
90
+ replay: any;
90
91
  };
91
92
  /**
92
93
  * Executes a function once and caches the result. If the function is called
@@ -76,6 +76,10 @@ class WorkflowService {
76
76
  const COUNTER = store.get('counter');
77
77
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
78
78
  const sessionId = `-start${workflowDimension}-${execIndex}-`;
79
+ const replay = store.get('replay');
80
+ if (sessionId in replay) {
81
+ return replay[sessionId];
82
+ }
79
83
  //NOTE: this is the hash prefix; necessary for the search index to locate the entity
80
84
  const entityOrEmptyString = options.entity ?? '';
81
85
  //If the workflowId is not provided, it is generated from the entity and the workflow name
@@ -172,6 +176,8 @@ class WorkflowService {
172
176
  static getContext() {
173
177
  const store = storage_1.asyncLocalStorage.getStore();
174
178
  const workflowId = store.get('workflowId');
179
+ const replay = store.get('replay');
180
+ const cursor = store.get('cursor');
175
181
  const workflowDimension = store.get('workflowDimension') ?? '';
176
182
  const workflowTopic = store.get('workflowTopic');
177
183
  const namespace = store.get('namespace');
@@ -180,7 +186,9 @@ class WorkflowService {
180
186
  const COUNTER = store.get('counter');
181
187
  return {
182
188
  counter: COUNTER.counter,
189
+ cursor,
183
190
  namespace,
191
+ replay,
184
192
  workflowId,
185
193
  workflowDimension,
186
194
  workflowTopic,
@@ -201,6 +209,10 @@ class WorkflowService {
201
209
  const COUNTER = store.get('counter');
202
210
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
203
211
  const sessionId = `-${prefix}${workflowDimension}-${execIndex}-`;
212
+ const replay = store.get('replay');
213
+ if (sessionId in replay) {
214
+ return false;
215
+ }
204
216
  const keyParams = {
205
217
  appId: hotMeshClient.appId,
206
218
  jobId: workflowId
@@ -274,6 +286,7 @@ class WorkflowService {
274
286
  workflowTopic: store.get('workflowTopic'),
275
287
  workflowDimension: store.get('workflowDimension') ?? '',
276
288
  counter: store.get('counter'),
289
+ replay: store.get('replay'),
277
290
  };
278
291
  }
279
292
  /**
@@ -284,9 +297,12 @@ class WorkflowService {
284
297
  * @template T - the result type
285
298
  */
286
299
  static async once(fn, ...args) {
287
- const { workflowId, namespace, workflowTopic, workflowDimension, counter: COUNTER, } = WorkflowService.getLocalState();
300
+ const { workflowId, namespace, workflowTopic, workflowDimension, counter: COUNTER, replay, } = WorkflowService.getLocalState();
288
301
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
289
302
  const sessionId = `-once${workflowDimension}-${execIndex}-`;
303
+ if (sessionId in replay) {
304
+ return JSON.parse(replay[sessionId]);
305
+ }
290
306
  const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
291
307
  const keyParams = {
292
308
  appId: hotMeshClient.appId,
@@ -65,7 +65,7 @@ declare class EngineService {
65
65
  resolveQuery(topic: string, query: JobStatsInput): Promise<GetStatsOptions>;
66
66
  processStreamMessage(streamData: StreamDataResponse): Promise<void>;
67
67
  execAdjacentParent(context: JobState, jobOutput: JobOutput, emit?: boolean): Promise<string>;
68
- hasParentJob(context: JobState): boolean;
68
+ hasParentJob(context: JobState, checkSevered?: boolean): boolean;
69
69
  resolveError(metadata: JobMetadata): StreamError | undefined;
70
70
  interrupt(topic: string, jobId: string, options?: JobInterruptOptions): Promise<string>;
71
71
  scrub(jobId: string): Promise<void>;
@@ -259,6 +259,7 @@ class EngineService {
259
259
  pg: streamData.metadata.gid,
260
260
  pd: streamData.metadata.dad,
261
261
  pa: streamData.metadata.aid,
262
+ px: streamData.metadata.await === false,
262
263
  trc: streamData.metadata.trc,
263
264
  spn: streamData.metadata.spn,
264
265
  };
@@ -316,7 +317,10 @@ class EngineService {
316
317
  return (await this.router?.publishMessage(null, streamData));
317
318
  }
318
319
  }
319
- hasParentJob(context) {
320
+ hasParentJob(context, checkSevered = false) {
321
+ if (checkSevered) {
322
+ return Boolean(context.metadata.pj && context.metadata.pa && !context.metadata.px);
323
+ }
320
324
  return Boolean(context.metadata.pj && context.metadata.pa);
321
325
  }
322
326
  resolveError(metadata) {
@@ -385,7 +389,12 @@ class EngineService {
385
389
  const workItems = await reporter.getWorkItems(resolvedQuery, queryFacets);
386
390
  if (workItems.length) {
387
391
  const taskService = new task_1.TaskService(this.store, this.logger);
388
- await taskService.enqueueWorkItems(workItems.map(workItem => `${hookTopic}::${workItem}::${keyResolver.scrub || false}::${JSON.stringify(data)}`));
392
+ await taskService.enqueueWorkItems(workItems.map(workItem => [
393
+ hookTopic,
394
+ workItem,
395
+ keyResolver.scrub || false,
396
+ JSON.stringify(data)
397
+ ].join(key_1.VALSEP)));
389
398
  this.store.publish(key_1.KeyType.QUORUM, { type: 'work', originator: this.guid }, this.appId);
390
399
  }
391
400
  return workItems;
@@ -504,7 +513,7 @@ class EngineService {
504
513
  // ********** JOB COMPLETION/CLEANUP (AND JOB EMIT) ***********
505
514
  async runJobCompletionTasks(context, options = {}) {
506
515
  //'emit' indicates the job is still active
507
- const isAwait = this.hasParentJob(context);
516
+ const isAwait = this.hasParentJob(context, true);
508
517
  const isOneTimeSub = this.hasOneTimeSubscription(context);
509
518
  const topic = await this.getPublishesTopic(context);
510
519
  let msgId;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ExporterService = void 0;
4
4
  const serializer_1 = require("../serializer");
5
5
  const utils_1 = require("../../modules/utils");
6
+ const key_1 = require("../../modules/key");
6
7
  /**
7
8
  * Downloads job data from Redis (hscan, hmget, hgetall)
8
9
  * Expands process data and includes dependency list
@@ -87,8 +88,8 @@ class ExporterService {
87
88
  const hookReg = /([0-9,]+)-(\d+)$/;
88
89
  const flowReg = /-(\d+)$/;
89
90
  return data.map((dependency, index) => {
90
- const [action, topic, gid, ...jid] = dependency.split('::');
91
- const jobId = jid.join('::');
91
+ const [action, topic, gid, _pd, ...jid] = dependency.split(key_1.VALSEP);
92
+ const jobId = jid.join(key_1.VALSEP);
92
93
  const match = jobId.match(hookReg);
93
94
  let prefix;
94
95
  let type;
@@ -167,6 +167,10 @@ class HotMeshService {
167
167
  }
168
168
  stop() {
169
169
  this.engine?.taskService.cancelCleanup();
170
+ this.quorum?.stop();
171
+ this.workers?.forEach((worker) => {
172
+ worker.stop();
173
+ });
170
174
  }
171
175
  async compress(terms) {
172
176
  return await this.engine?.compress(terms);