@hotmeshio/hotmesh 0.0.37 → 0.0.39

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 (78) hide show
  1. package/README.md +14 -8
  2. package/build/modules/enums.d.ts +29 -23
  3. package/build/modules/enums.js +38 -29
  4. package/build/modules/errors.d.ts +1 -1
  5. package/build/modules/errors.js +9 -7
  6. package/build/modules/key.d.ts +1 -34
  7. package/build/modules/key.js +24 -47
  8. package/build/package.json +1 -1
  9. package/build/services/activities/activity.js +1 -1
  10. package/build/services/activities/hook.js +4 -9
  11. package/build/services/activities/trigger.d.ts +3 -2
  12. package/build/services/activities/trigger.js +10 -6
  13. package/build/services/durable/client.d.ts +9 -1
  14. package/build/services/durable/client.js +30 -14
  15. package/build/services/durable/handle.js +2 -2
  16. package/build/services/durable/worker.js +4 -3
  17. package/build/services/engine/index.d.ts +2 -1
  18. package/build/services/engine/index.js +6 -6
  19. package/build/services/hotmesh/index.d.ts +2 -2
  20. package/build/services/hotmesh/index.js +3 -4
  21. package/build/services/quorum/index.d.ts +6 -6
  22. package/build/services/quorum/index.js +47 -11
  23. package/build/services/router/index.js +16 -14
  24. package/build/services/store/clients/ioredis.d.ts +1 -0
  25. package/build/services/store/clients/ioredis.js +9 -0
  26. package/build/services/store/clients/redis.d.ts +1 -0
  27. package/build/services/store/clients/redis.js +16 -0
  28. package/build/services/store/index.d.ts +15 -9
  29. package/build/services/store/index.js +46 -23
  30. package/build/services/stream/clients/ioredis.d.ts +1 -0
  31. package/build/services/stream/clients/ioredis.js +33 -24
  32. package/build/services/stream/clients/redis.d.ts +1 -0
  33. package/build/services/stream/clients/redis.js +15 -0
  34. package/build/services/stream/index.d.ts +1 -0
  35. package/build/services/task/index.d.ts +10 -3
  36. package/build/services/task/index.js +35 -17
  37. package/build/services/worker/index.d.ts +1 -0
  38. package/build/services/worker/index.js +24 -0
  39. package/build/types/durable.d.ts +3 -2
  40. package/build/types/hotmesh.d.ts +43 -2
  41. package/build/types/hotmesh.js +28 -0
  42. package/build/types/index.d.ts +3 -2
  43. package/build/types/index.js +3 -1
  44. package/build/types/logger.d.ts +1 -0
  45. package/build/types/logger.js +1 -0
  46. package/build/types/quorum.d.ts +11 -1
  47. package/build/types/redisclient.d.ts +1 -0
  48. package/build/types/task.d.ts +1 -0
  49. package/build/types/task.js +2 -0
  50. package/modules/enums.ts +49 -35
  51. package/modules/errors.ts +17 -8
  52. package/modules/key.ts +3 -40
  53. package/package.json +1 -1
  54. package/services/activities/activity.ts +2 -2
  55. package/services/activities/hook.ts +18 -9
  56. package/services/activities/trigger.ts +10 -6
  57. package/services/durable/client.ts +31 -15
  58. package/services/durable/handle.ts +3 -3
  59. package/services/durable/worker.ts +4 -3
  60. package/services/engine/index.ts +13 -12
  61. package/services/hotmesh/index.ts +4 -5
  62. package/services/quorum/index.ts +48 -12
  63. package/services/router/index.ts +26 -24
  64. package/services/store/clients/ioredis.ts +9 -0
  65. package/services/store/clients/redis.ts +16 -0
  66. package/services/store/index.ts +63 -25
  67. package/services/stream/clients/ioredis.ts +33 -24
  68. package/services/stream/clients/redis.ts +14 -0
  69. package/services/stream/index.ts +1 -0
  70. package/services/task/index.ts +66 -24
  71. package/services/worker/index.ts +30 -0
  72. package/types/durable.ts +6 -5
  73. package/types/hotmesh.ts +47 -2
  74. package/types/index.ts +8 -1
  75. package/types/logger.ts +3 -1
  76. package/types/quorum.ts +15 -4
  77. package/types/redisclient.ts +1 -0
  78. package/types/task.ts +1 -0
package/modules/errors.ts CHANGED
@@ -1,9 +1,18 @@
1
1
  import { ActivityDuplex } from "../types/activity";
2
2
  import { CollationFaultType, CollationStage } from "../types/collator";
3
+ import {
4
+ HMSH_CODE_DURABLE_MAXED,
5
+ HMSH_CODE_DURABLE_TIMEOUT,
6
+ HMSH_CODE_DURABLE_FATAL,
7
+ HMSH_CODE_DURABLE_INCOMPLETE,
8
+ HMSH_CODE_NOTFOUND,
9
+ HMSH_CODE_DURABLE_RETRYABLE,
10
+ HMSH_CODE_DURABLE_SLEEPFOR,
11
+ HMSH_CODE_DURABLE_WAITFOR } from "./enums";
3
12
 
4
13
  class GetStateError extends Error {
5
14
  jobId: string;
6
- code: 404;
15
+ code = HMSH_CODE_NOTFOUND;
7
16
  constructor(jobId: string) {
8
17
  super(`${jobId} Not Found`);
9
18
  this.jobId = jobId;
@@ -21,7 +30,7 @@ class DurableIncompleteSignalError extends Error {
21
30
  code: number;
22
31
  constructor(message: string) {
23
32
  super(message);
24
- this.code = 593;
33
+ this.code = HMSH_CODE_DURABLE_INCOMPLETE;
25
34
  }
26
35
  }
27
36
 
@@ -32,7 +41,7 @@ class DurableWaitForSignalError extends Error {
32
41
  constructor(message: string, signals: {signal: string, index: number}[]) {
33
42
  super(message);
34
43
  this.signals = signals;
35
- this.code = 594;
44
+ this.code = HMSH_CODE_DURABLE_WAITFOR;
36
45
  }
37
46
  }
38
47
 
@@ -60,35 +69,35 @@ class DurableSleepForError extends Error {
60
69
  this.duration = duration;
61
70
  this.index = index;
62
71
  this.dimension = dimension;
63
- this.code = 592;
72
+ this.code = HMSH_CODE_DURABLE_SLEEPFOR;
64
73
  }
65
74
  }
66
75
  class DurableTimeoutError extends Error {
67
76
  code: number;
68
77
  constructor(message: string) {
69
78
  super(message);
70
- this.code = 596;
79
+ this.code = HMSH_CODE_DURABLE_TIMEOUT;
71
80
  }
72
81
  }
73
82
  class DurableMaxedError extends Error {
74
83
  code: number;
75
84
  constructor(message: string) {
76
85
  super(message);
77
- this.code = 597;
86
+ this.code = HMSH_CODE_DURABLE_MAXED;
78
87
  }
79
88
  }
80
89
  class DurableFatalError extends Error {
81
90
  code: number;
82
91
  constructor(message: string) {
83
92
  super(message);
84
- this.code = 598;
93
+ this.code = HMSH_CODE_DURABLE_FATAL;
85
94
  }
86
95
  }
87
96
  class DurableRetryError extends Error {
88
97
  code: number;
89
98
  constructor(message: string) {
90
99
  super(message);
91
- this.code = 599;
100
+ this.code = HMSH_CODE_DURABLE_RETRYABLE;
92
101
  }
93
102
  }
94
103
 
package/modules/key.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { KeyStoreParams, KeyType } from '../types/hotmesh';
2
+
1
3
  /**
2
4
  * Keys
3
5
  *
@@ -26,46 +28,7 @@
26
28
  * hmsh:<appid>:sym:vals: -> {hash} list of symbols for job values across all app versions
27
29
  */
28
30
 
29
- //default namespace for hotmesh
30
- const HMNS = "hmsh";
31
-
32
- //these are the entity types that are stored in the key/value store
33
- enum KeyType {
34
- APP = 'APP',
35
- ENGINE_ID = 'ENGINE',
36
- HOOKS = 'HOOKS',
37
- JOB_DEPENDENTS = 'JOB_DEPENDENTS',
38
- JOB_STATE = 'JOB_STATE',
39
- JOB_STATS_GENERAL = 'JOB_STATS_GENERAL',
40
- JOB_STATS_MEDIAN = 'JOB_STATS_MEDIAN',
41
- JOB_STATS_INDEX = 'JOB_STATS_INDEX',
42
- HOTMESH = 'HOTMESH',
43
- QUORUM = 'QUORUM',
44
- SCHEMAS = 'SCHEMAS',
45
- SIGNALS = 'SIGNALS',
46
- STREAMS = 'STREAMS',
47
- SUBSCRIPTIONS = 'SUBSCRIPTIONS',
48
- SUBSCRIPTION_PATTERNS = 'SUBSCRIPTION_PATTERNS',
49
- SYMKEYS = 'SYMKEYS',
50
- SYMVALS = 'SYMVALS',
51
- TIME_RANGE = 'TIME_RANGE',
52
- WORK_ITEMS = 'WORK_ITEMS',
53
- }
54
-
55
- //when minting a key, the following parameters are used to create a unique key per entity
56
- type KeyStoreParams = {
57
- appId?: string; //app id is a uuid for a hotmesh app
58
- engineId?: string; //unique auto-generated guid for an ephemeral engine instance
59
- appVersion?: string; //(e.g. "1.0.0", "1", "1.0")
60
- jobId?: string; //a customer-defined id for job; must be unique for the entire app
61
- activityId?: string; //activity id is a uuid for a given hotmesh app
62
- jobKey?: string; //a customer-defined label for a job that serves to categorize events
63
- dateTime?: string; //UTC date time: YYYY-MM-DDTHH:MM (20203-04-12T00:00); serves as a time-series bucket for the job_key
64
- facet?: string; //data path starting at root with values separated by colons (e.g. "object/type:bar")
65
- topic?: string; //topic name (e.g., "foo" or "" for top-level)
66
- timeValue?: number; //time value (rounded to minute) (for delete range)
67
- scoutType?: 'signal' | 'time'; //a single member of the quorum serves as the 'scout' for the group, triaging tasks for the collective
68
- };
31
+ const HMNS = "hmsh"; //default
69
32
 
70
33
  class KeyService {
71
34
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.37",
3
+ "version": "0.0.39",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -1,4 +1,4 @@
1
- import { EXPIRE_DURATION } from '../../modules/enums';
1
+ import { HMSH_EXPIRE_DURATION } from '../../modules/enums';
2
2
  import {
3
3
  CollationError,
4
4
  GenerationalError,
@@ -435,7 +435,7 @@ class Activity {
435
435
 
436
436
  initPolicies(context: JobState) {
437
437
  const expire = Pipe.resolve(
438
- this.config.expire ?? EXPIRE_DURATION,
438
+ this.config.expire ?? HMSH_EXPIRE_DURATION,
439
439
  context
440
440
  );
441
441
  context.metadata.expire = expire;
@@ -131,16 +131,25 @@ class Hook extends Activity {
131
131
 
132
132
  async registerHook(multi?: RedisMulti): Promise<string | void> {
133
133
  if (this.config.hook?.topic) {
134
- const taskService = new TaskService(this.store, this.logger);
135
- return await taskService.registerWebHook(this.config.hook.topic, this.context, this.resolveDad(), multi);
134
+ return await this.engine.taskService.registerWebHook(
135
+ this.config.hook.topic,
136
+ this.context,
137
+ this.resolveDad(),
138
+ multi
139
+ );
136
140
  } else if (this.config.sleep) {
137
- const durationInSeconds = Pipe.resolve(this.config.sleep, this.context);
138
- const jobId = this.context.metadata.jid;
139
- const gId = this.context.metadata.gid;
140
- const activityId = this.metadata.aid;
141
- const dId = this.metadata.dad;
142
- await this.engine.taskService.registerTimeHook(jobId, gId, `${activityId}${dId||''}`, 'sleep', durationInSeconds);
143
- return jobId;
141
+ const duration = Pipe.resolve(
142
+ this.config.sleep,
143
+ this.context,
144
+ );
145
+ await this.engine.taskService.registerTimeHook(
146
+ this.context.metadata.jid,
147
+ this.context.metadata.gid,
148
+ `${this.metadata.aid}${this.metadata.dad || ''}`,
149
+ 'sleep',
150
+ duration,
151
+ );
152
+ return this.context.metadata.jid;
144
153
  }
145
154
  }
146
155
 
@@ -47,7 +47,7 @@ class Trigger extends Activity {
47
47
  const multi = this.store.getMulti();
48
48
  await this.setState(multi);
49
49
  await this.setStats(multi);
50
- await this.setDependency(multi);
50
+ await this.registerJobDependency(multi);
51
51
  await multi.exec();
52
52
 
53
53
  telemetry.mapActivityAttributes();
@@ -180,13 +180,17 @@ class Trigger extends Activity {
180
180
  }
181
181
 
182
182
  /**
183
- * Registers this job as a dependent of the parent job
183
+ * Registers this job as a dependent of the parent job; when the
184
+ * parent job is interrupted, this job will be interrupted
184
185
  */
185
- async setDependency(multi?: RedisMulti): Promise<void> {
186
- const depKey = this.config.stats?.parent;
187
- const resolvedDepKey = depKey ? Pipe.resolve(depKey, this.context) : '';
186
+ async registerJobDependency(multi?: RedisMulti): Promise<void> {
187
+ const depKey = this.config.stats?.parent ?? this.context.metadata.pj;
188
+ let resolvedDepKey = depKey ? Pipe.resolve(depKey, this.context) : '';
189
+ if (!resolvedDepKey) {
190
+ resolvedDepKey = this.context.metadata.pj;
191
+ }
188
192
  if (resolvedDepKey) {
189
- await this.store.setDependency(
193
+ await this.store.registerJobDependency(
190
194
  resolvedDepKey,
191
195
  this.context.metadata.tpc,
192
196
  this.context.metadata.jid,
@@ -11,11 +11,12 @@ import { JobState } from '../../types/job';
11
11
  import { KeyService, KeyType } from '../../modules/key';
12
12
  import { Search } from './search';
13
13
  import { StreamStatus } from '../../types';
14
- import { DURABLE_EXPIRE_SECONDS } from '../../modules/enums';
14
+ import { HMSH_LOGLEVEL, HMSH_EXPIRE_JOB_SECONDS } from '../../modules/enums';
15
15
 
16
16
  export class ClientService {
17
17
 
18
18
  connection: Connection;
19
+ topics: string[] = [];
19
20
  options: WorkflowOptions;
20
21
  static instances = new Map<string, HotMesh | Promise<HotMesh>>();
21
22
 
@@ -23,14 +24,22 @@ export class ClientService {
23
24
  this.connection = config.connection;
24
25
  }
25
26
 
26
- getHotMeshClient = async (worflowTopic: string, namespace?: string) => {
27
- //NOTE: every unique topic inits a new engine
28
- if (ClientService.instances.has(worflowTopic)) {
29
- return await ClientService.instances.get(worflowTopic);
27
+ getHotMeshClient = async (workflowTopic: string, namespace?: string) => {
28
+ //use the cached instance
29
+ const instanceId = 'SINGLETON';
30
+ if (ClientService.instances.has(instanceId)) {
31
+ const hotMeshClient = await ClientService.instances.get(instanceId);
32
+ if (!this.topics.includes(workflowTopic)) {
33
+ this.topics.push(workflowTopic);
34
+ await this.createStream(hotMeshClient, workflowTopic, namespace);
35
+ }
36
+ return hotMeshClient;
30
37
  }
31
38
 
39
+ //create and cache an instance
32
40
  const hotMeshClient = HotMesh.init({
33
41
  appId: namespace ?? APP_ID,
42
+ logLevel: HMSH_LOGLEVEL,
34
43
  engine: {
35
44
  redis: {
36
45
  class: this.connection.class,
@@ -38,19 +47,27 @@ export class ClientService {
38
47
  }
39
48
  }
40
49
  });
41
- ClientService.instances.set(worflowTopic, hotMeshClient);
50
+ ClientService.instances.set(instanceId, hotMeshClient);
51
+ await this.createStream(await hotMeshClient, workflowTopic, namespace);
52
+ await this.activateWorkflow(await hotMeshClient, namespace ?? APP_ID);
53
+ return hotMeshClient;
54
+ }
42
55
 
43
- //since the YAML topic is dynamic, it MUST be manually created before use
44
- const store = (await hotMeshClient).engine.store;
45
- const params = { appId: namespace ?? APP_ID, topic: worflowTopic };
56
+ /**
57
+ * Creates a stream (Redis `XGROUP.CREATE`) where events can be published (XADD).
58
+ * It is possible that the worker that will read from this stream channel
59
+ * has not yet been initialized, so this call ensures that the channel
60
+ * exists and is ready to serve as a container for events.
61
+ */
62
+ createStream = async(hotMeshClient: HotMesh, workflowTopic: string, namespace?: string) => {
63
+ const store = hotMeshClient.engine.store;
64
+ const params = { appId: namespace ?? APP_ID, topic: workflowTopic };
46
65
  const streamKey = store.mintKey(KeyType.STREAMS, params);
47
66
  try {
48
67
  await store.xgroup('CREATE', streamKey, 'WORKER', '$', 'MKSTREAM');
49
68
  } catch (err) {
50
69
  //ignore if already exists
51
70
  }
52
- await this.activateWorkflow(await hotMeshClient, namespace ?? APP_ID);
53
- return hotMeshClient;
54
71
  }
55
72
 
56
73
  /**
@@ -105,7 +122,7 @@ export class ClientService {
105
122
  const payload = {
106
123
  arguments: [...options.args],
107
124
  originJobId: options.originJobId,
108
- expire: options.expire ?? DURABLE_EXPIRE_SECONDS,
125
+ expire: options.expire ?? HMSH_EXPIRE_JOB_SECONDS,
109
126
  parentWorkflowId: options.parentWorkflowId,
110
127
  workflowId: options.workflowId || HotMesh.guid(),
111
128
  workflowTopic: workflowTopic,
@@ -155,9 +172,8 @@ export class ClientService {
155
172
  if (options.search?.data) {
156
173
  const searchSessionId = `-search-${HotMesh.guid()}-0`;
157
174
  const search = new Search(options.workflowId, hotMeshClient, searchSessionId);
158
- for (const [key, value] of Object.entries(options.search.data)) {
159
- search.set(key, value);
160
- }
175
+ const entries = Object.entries(options.search.data).flat();
176
+ await search.set(...entries);
161
177
  }
162
178
  return msgId;
163
179
  },
@@ -1,4 +1,4 @@
1
- import { STATUS_CODE_INTERRUPT } from '../../modules/enums';
1
+ import { HMSH_CODE_INTERRUPT } from '../../modules/enums';
2
2
  import { HotMeshService as HotMesh } from '../hotmesh';
3
3
  import { JobInterruptOptions, JobOutput } from '../../types/job';
4
4
  import { StreamError } from '../../types/stream';
@@ -101,7 +101,7 @@ export class WorkflowHandleService {
101
101
  const state = await this.hotMesh.getState(`${this.hotMesh.appId}.execute`, this.workflowId);
102
102
  if (state.metadata.err) {
103
103
  const error = JSON.parse(state.metadata.err) as StreamError;
104
- if (error.code === STATUS_CODE_INTERRUPT || !state.data) {
104
+ if (error.code === HMSH_CODE_INTERRUPT || !state.data) {
105
105
  return reject({ ...error, job_id: this.workflowId });
106
106
  }
107
107
  }
@@ -117,7 +117,7 @@ export class WorkflowHandleService {
117
117
  this.hotMesh.sub(topic, async (topic: string, state: JobOutput) => {
118
118
  if (state.metadata.err) {
119
119
  const error = JSON.parse(state.metadata.err) as StreamError;
120
- if (error.code === STATUS_CODE_INTERRUPT || !state.data) {
120
+ if (error.code === HMSH_CODE_INTERRUPT || !state.data) {
121
121
  return await complete(null, state.metadata.err);
122
122
  }
123
123
  }
@@ -23,6 +23,7 @@ import {
23
23
  StreamData,
24
24
  StreamDataResponse,
25
25
  StreamStatus } from '../../types/stream';
26
+ import { HMSH_LOGLEVEL } from '../../modules/enums';
26
27
 
27
28
  export class WorkerService {
28
29
  static activityRegistry: Registry = {}; //user's activities
@@ -36,7 +37,7 @@ export class WorkerService {
36
37
  return await WorkerService.instances.get(workflowTopic);
37
38
  }
38
39
  const hotMeshClient = HotMesh.init({
39
- logLevel: options?.logLevel as 'debug' ?? 'info',
40
+ logLevel: options?.logLevel ?? HMSH_LOGLEVEL,
40
41
  appId: config.namespace ?? APP_ID,
41
42
  engine: { redis: { ...WorkerService.connection } }
42
43
  });
@@ -121,7 +122,7 @@ export class WorkerService {
121
122
  options: config.connection.options as RedisOptions
122
123
  };
123
124
  const hotMeshWorker = await HotMesh.init({
124
- logLevel: config.options?.logLevel as 'debug' ?? 'info',
125
+ logLevel: config.options?.logLevel ?? HMSH_LOGLEVEL,
125
126
  appId: config.namespace ?? APP_ID,
126
127
  engine: { redis: redisConfig },
127
128
  workers: [
@@ -172,7 +173,7 @@ export class WorkerService {
172
173
  options: config.connection.options as RedisOptions
173
174
  };
174
175
  const hotMeshWorker = await HotMesh.init({
175
- logLevel: config.options?.logLevel as 'debug' ?? 'info',
176
+ logLevel: config.options?.logLevel ?? HMSH_LOGLEVEL,
176
177
  appId: config.namespace ?? APP_ID,
177
178
  engine: { redis: redisConfig },
178
179
  workers: [{
@@ -1,10 +1,10 @@
1
1
  import { KeyType } from '../../modules/key';
2
2
  import {
3
- OTT_WAIT_TIME,
4
- STATUS_CODE_SUCCESS,
5
- STATUS_CODE_PENDING,
6
- STATUS_CODE_TIMEOUT,
7
- DURABLE_EXPIRE_SECONDS } from '../../modules/enums';
3
+ HMSH_OTT_WAIT_TIME,
4
+ HMSH_CODE_SUCCESS,
5
+ HMSH_CODE_PENDING,
6
+ HMSH_CODE_TIMEOUT,
7
+ HMSH_EXPIRE_JOB_SECONDS } from '../../modules/enums';
8
8
  import {
9
9
  formatISODate,
10
10
  getSubscriptionTopic,
@@ -77,6 +77,7 @@ import {
77
77
  StreamError,
78
78
  StreamRole,
79
79
  StreamStatus } from '../../types/stream';
80
+ import { WorkListTaskType } from '../../types/task';
80
81
 
81
82
  class EngineService {
82
83
  namespace: string;
@@ -438,10 +439,10 @@ class EngineService {
438
439
  streamData.code = error.code;
439
440
  } else if (emit) {
440
441
  streamData.status = StreamStatus.PENDING;
441
- streamData.code = STATUS_CODE_PENDING;
442
+ streamData.code = HMSH_CODE_PENDING;
442
443
  } else {
443
444
  streamData.status = StreamStatus.SUCCESS;
444
- streamData.code = STATUS_CODE_SUCCESS;
445
+ streamData.code = HMSH_CODE_SUCCESS;
445
446
  }
446
447
  return (await this.router?.publishMessage(null, streamData)) as string;
447
448
  }
@@ -489,7 +490,7 @@ class EngineService {
489
490
  };
490
491
  return await this.router.publishMessage(null, streamData) as string;
491
492
  }
492
- async hookTime(jobId: string, gId: string, activityId: string, type?: 'sleep'|'expire'|'interrupt'): Promise<string | void> {
493
+ async hookTime(jobId: string, gId: string, activityId: string, type?: WorkListTaskType): Promise<string | void> {
493
494
  if (type === 'interrupt') {
494
495
  return await this.interrupt(
495
496
  activityId, //note: 'activityId' is the actually job topic
@@ -499,7 +500,6 @@ class EngineService {
499
500
  } else if (type === 'expire') {
500
501
  return await this.store.expireJob(jobId, 1);
501
502
  }
502
- //'sleep': parse the activityId into parts
503
503
  const [aid, ...dimensions] = activityId.split(',');
504
504
  const dad = `,${dimensions.join(',')}`;
505
505
  const streamData: StreamData = {
@@ -575,7 +575,7 @@ class EngineService {
575
575
  return await this.subscribe.punsubscribe(KeyType.QUORUM, this.appId, wild);
576
576
  }
577
577
  //publish and await (returns the job and data (if ready)); throws error with jobid if not
578
- async pubsub(topic: string, data: JobData, context?: JobState | null, timeout = OTT_WAIT_TIME): Promise<JobOutput> {
578
+ async pubsub(topic: string, data: JobData, context?: JobState | null, timeout = HMSH_OTT_WAIT_TIME): Promise<JobOutput> {
579
579
  context = {
580
580
  metadata: {
581
581
  ngn: this.guid,
@@ -597,9 +597,10 @@ class EngineService {
597
597
  }
598
598
  });
599
599
  setTimeout(() => {
600
+ //note: job is still active (the subscriber timed out)
600
601
  this.delistJobCallback(jobId);
601
602
  reject({
602
- code: STATUS_CODE_TIMEOUT,
603
+ code: HMSH_CODE_TIMEOUT,
603
604
  message: 'timeout',
604
605
  job_id: jobId
605
606
  } as StreamError);
@@ -685,7 +686,7 @@ class EngineService {
685
686
  * it will be expired immediately.
686
687
  */
687
688
  resolveExpires(context: JobState, options: JobCompletionOptions): number {
688
- return options.expire ?? context.metadata.expire ?? DURABLE_EXPIRE_SECONDS;
689
+ return options.expire ?? context.metadata.expire ?? HMSH_EXPIRE_JOB_SECONDS;
689
690
  }
690
691
 
691
692
 
@@ -16,7 +16,7 @@ import {
16
16
  import {
17
17
  HotMeshConfig,
18
18
  HotMeshManifest } from '../../types/hotmesh';
19
- import { JobMessageCallback } from '../../types/quorum';
19
+ import { JobMessageCallback, QuorumProfile } from '../../types/quorum';
20
20
  import {
21
21
  JobStatsInput,
22
22
  GetStatsOptions,
@@ -137,6 +137,9 @@ class HotMeshService {
137
137
  }
138
138
 
139
139
  // ************* COMPILER METHODS *************
140
+ async rollCall(delay?: number): Promise<QuorumProfile[]> {
141
+ return await this.quorum?.rollCall(delay);
142
+ }
140
143
  async plan(path: string): Promise<HotMeshManifest> {
141
144
  return await this.engine?.plan(path);
142
145
  }
@@ -147,10 +150,6 @@ class HotMeshService {
147
150
  //activation is a quorum operation
148
151
  return await this.quorum?.activate(version, delay);
149
152
  }
150
- async inventory(version: string, delay?: number): Promise<number> {
151
- //get count of all peers
152
- return await this.quorum?.inventory(delay);
153
- }
154
153
 
155
154
  // ************* REPORTER METHODS *************
156
155
  async getStats(topic: string, query: JobStatsInput): Promise<StatsResponse> {
@@ -14,6 +14,7 @@ import { RedisClientType as IORedisClientType } from '../../types/ioredisclient'
14
14
  import {
15
15
  QuorumMessage,
16
16
  QuorumMessageCallback,
17
+ QuorumProfile,
17
18
  SubscriptionCallback,
18
19
  ThrottleMessage
19
20
  } from '../../types/quorum';
@@ -26,10 +27,10 @@ const QUORUM_DELAY = 250;
26
27
 
27
28
  class QuorumService {
28
29
  namespace: string;
29
- apps: HotMeshApps | null;
30
30
  appId: string;
31
31
  guid: string;
32
32
  engine: EngineService;
33
+ profiles: QuorumProfile[] = [];
33
34
  store: StoreService<RedisClient, RedisMulti> | null;
34
35
  subscribe: SubService<RedisClient, RedisMulti> | null;
35
36
  logger: ILogger;
@@ -108,9 +109,12 @@ class QuorumService {
108
109
  if (message.type === 'activate') {
109
110
  self.engine.setCacheMode(message.cache_mode, message.until_version);
110
111
  } else if (message.type === 'ping') {
111
- this.sayPong(self.appId, self.guid, message.originator);
112
+ self.sayPong(self.appId, self.guid, message.originator, message.details);
112
113
  } else if (message.type === 'pong' && self.guid === message.originator) {
113
114
  self.quorum = self.quorum + 1;
115
+ if (message.profile) {
116
+ self.profiles.push(message.profile);
117
+ }
114
118
  } else if (message.type === 'throttle') {
115
119
  self.engine.throttle(message.throttle);
116
120
  } else if (message.type === 'work') {
@@ -127,20 +131,38 @@ class QuorumService {
127
131
  };
128
132
  }
129
133
 
130
- async sayPong(appId: string, guid: string, originator: string) {
134
+ async sayPong(appId: string, guid: string, originator: string, details = false) {
135
+ let profile: QuorumProfile;
136
+ if (details) {
137
+ profile = {
138
+ engine_id: this.guid,
139
+ namespace: this.namespace,
140
+ app_id: this.appId,
141
+ stream: this.engine.stream.mintKey(KeyType.STREAMS, { appId: this.appId })
142
+ };
143
+ }
131
144
  this.store.publish(
132
145
  KeyType.QUORUM,
133
- { type: 'pong', guid, originator },
146
+ {
147
+ type: 'pong',
148
+ guid, originator,
149
+ profile,
150
+ },
134
151
  appId,
135
152
  );
136
153
  }
137
154
 
138
- async requestQuorum(delay = QUORUM_DELAY): Promise<number> {
155
+ async requestQuorum(delay = QUORUM_DELAY, details = false): Promise<number> {
139
156
  const quorum = this.quorum;
140
157
  this.quorum = 0;
158
+ this.profiles.length = 0;
141
159
  await this.store.publish(
142
160
  KeyType.QUORUM,
143
- { type: 'ping', originator: this.guid },
161
+ {
162
+ type: 'ping',
163
+ originator: this.guid,
164
+ details,
165
+ },
144
166
  this.appId,
145
167
  );
146
168
  await sleepFor(delay);
@@ -166,12 +188,26 @@ class QuorumService {
166
188
 
167
189
 
168
190
  // ************* COMPILER METHODS *************
169
- async inventory(delay = QUORUM_DELAY): Promise<number> {
170
- await this.requestQuorum(delay);
171
- const q1 = await this.requestQuorum(delay);
172
- const q2 = await this.requestQuorum(delay);
173
- const q3 = await this.requestQuorum(delay);
174
- return Math.round((q1 + q2 + q3) / 3);
191
+ async rollCall(delay = QUORUM_DELAY): Promise<QuorumProfile[]> {
192
+ await this.requestQuorum(delay, true);
193
+ const targetStreams = [];
194
+ const multi = this.store.getMulti();
195
+ this.profiles.forEach((profile: QuorumProfile) => {
196
+ if (!targetStreams.includes(profile.stream)) {
197
+ targetStreams.push(profile.stream);
198
+ this.store.xlen(profile.stream, multi);
199
+ }
200
+ });
201
+ const stream_depths = await multi.exec() as number[];
202
+ this.profiles.forEach(async (profile: QuorumProfile) => {
203
+ const index = targetStreams.indexOf(profile.stream);
204
+ if (index != -1) {
205
+ profile.stream_depth = Array.isArray(stream_depths[index]) ?
206
+ stream_depths[index][1] :
207
+ stream_depths[index];
208
+ }
209
+ });
210
+ return this.profiles;
175
211
  }
176
212
  async activate(version: string, delay = QUORUM_DELAY): Promise<boolean> {
177
213
  version = version.toString();