@hotmeshio/hotmesh 0.0.36 → 0.0.38

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 (89) hide show
  1. package/README.md +11 -11
  2. package/build/modules/enums.d.ts +1 -0
  3. package/build/modules/enums.js +3 -1
  4. package/build/modules/errors.d.ts +9 -1
  5. package/build/modules/errors.js +12 -1
  6. package/build/modules/key.d.ts +20 -19
  7. package/build/modules/key.js +20 -20
  8. package/build/package.json +1 -1
  9. package/build/services/activities/activity.d.ts +10 -0
  10. package/build/services/activities/activity.js +28 -3
  11. package/build/services/activities/await.js +10 -9
  12. package/build/services/activities/cycle.js +10 -9
  13. package/build/services/activities/hook.d.ts +7 -1
  14. package/build/services/activities/hook.js +61 -44
  15. package/build/services/activities/interrupt.js +10 -9
  16. package/build/services/activities/signal.js +7 -7
  17. package/build/services/activities/trigger.js +4 -2
  18. package/build/services/activities/worker.js +9 -8
  19. package/build/services/durable/meshos.js +2 -2
  20. package/build/services/durable/worker.js +2 -2
  21. package/build/services/durable/workflow.js +17 -17
  22. package/build/services/engine/index.d.ts +5 -7
  23. package/build/services/engine/index.js +53 -47
  24. package/build/services/hotmesh/index.d.ts +2 -2
  25. package/build/services/hotmesh/index.js +6 -7
  26. package/build/services/quorum/index.d.ts +6 -6
  27. package/build/services/quorum/index.js +47 -11
  28. package/build/services/{signaler/stream.d.ts → router/index.d.ts} +3 -3
  29. package/build/services/{signaler/stream.js → router/index.js} +6 -6
  30. package/build/services/serializer/index.js +1 -1
  31. package/build/services/store/clients/ioredis.d.ts +1 -0
  32. package/build/services/store/clients/ioredis.js +9 -0
  33. package/build/services/store/clients/redis.d.ts +1 -0
  34. package/build/services/store/clients/redis.js +16 -0
  35. package/build/services/store/index.d.ts +10 -4
  36. package/build/services/store/index.js +21 -10
  37. package/build/services/stream/clients/ioredis.d.ts +1 -0
  38. package/build/services/stream/clients/ioredis.js +33 -24
  39. package/build/services/stream/clients/redis.d.ts +1 -0
  40. package/build/services/stream/clients/redis.js +15 -0
  41. package/build/services/stream/index.d.ts +1 -0
  42. package/build/services/task/index.d.ts +13 -4
  43. package/build/services/task/index.js +115 -17
  44. package/build/services/telemetry/index.js +6 -6
  45. package/build/services/worker/index.d.ts +4 -3
  46. package/build/services/worker/index.js +32 -8
  47. package/build/types/job.d.ts +2 -0
  48. package/build/types/quorum.d.ts +11 -1
  49. package/build/types/redisclient.d.ts +1 -0
  50. package/build/types/stream.d.ts +1 -0
  51. package/modules/enums.ts +3 -0
  52. package/modules/errors.ts +18 -0
  53. package/modules/key.ts +21 -20
  54. package/package.json +1 -1
  55. package/services/activities/activity.ts +44 -4
  56. package/services/activities/await.ts +14 -10
  57. package/services/activities/cycle.ts +14 -10
  58. package/services/activities/hook.ts +70 -47
  59. package/services/activities/interrupt.ts +13 -10
  60. package/services/activities/signal.ts +11 -8
  61. package/services/activities/trigger.ts +5 -1
  62. package/services/activities/worker.ts +13 -9
  63. package/services/durable/meshos.ts +1 -1
  64. package/services/durable/worker.ts +1 -1
  65. package/services/durable/workflow.ts +1 -1
  66. package/services/engine/index.ts +82 -44
  67. package/services/hotmesh/index.ts +7 -8
  68. package/services/quorum/index.ts +48 -12
  69. package/services/{signaler/stream.ts → router/index.ts} +5 -5
  70. package/services/serializer/index.ts +1 -1
  71. package/services/store/clients/ioredis.ts +9 -0
  72. package/services/store/clients/redis.ts +16 -0
  73. package/services/store/index.ts +27 -12
  74. package/services/stream/clients/ioredis.ts +33 -24
  75. package/services/stream/clients/redis.ts +14 -0
  76. package/services/stream/index.ts +1 -0
  77. package/services/task/index.ts +120 -21
  78. package/services/telemetry/index.ts +6 -6
  79. package/services/worker/index.ts +37 -7
  80. package/types/job.ts +2 -0
  81. package/types/quorum.ts +15 -4
  82. package/types/redisclient.ts +1 -0
  83. package/types/stream.ts +6 -5
  84. package/build/services/signaler/store.d.ts +0 -15
  85. package/build/services/signaler/store.js +0 -68
  86. package/services/signaler/store.ts +0 -76
  87. /package/build/{services/durable/asyncLocalStorage.d.ts → modules/storage.d.ts} +0 -0
  88. /package/build/{services/durable/asyncLocalStorage.js → modules/storage.js} +0 -0
  89. /package/{services/durable/asyncLocalStorage.ts → modules/storage.ts} +0 -0
@@ -29,7 +29,7 @@ import { Transitions } from '../../types/transition';
29
29
  import { formatISODate, getSymKey } from '../../modules/utils';
30
30
  import { ReclaimedMessageType } from '../../types/stream';
31
31
  import { JobCompletionOptions, JobInterruptOptions } from '../../types/job';
32
- import { STATUS_CODE_INTERRUPT } from '../../modules/enums';
32
+ import { SCOUT_INTERVAL_SECONDS, STATUS_CODE_INTERRUPT } from '../../modules/enums';
33
33
  import { GetStateError } from '../../modules/errors';
34
34
 
35
35
  interface AbstractRedisClient {
@@ -118,6 +118,10 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
118
118
  id: string,
119
119
  multi?: U
120
120
  ): Promise<number|U>;
121
+ abstract xlen(
122
+ key: string,
123
+ multi?: U
124
+ ): Promise<number|U>;
121
125
 
122
126
  constructor(redisClient: T) {
123
127
  this.redisClient = redisClient;
@@ -168,10 +172,19 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
168
172
  this.cache.invalidate();
169
173
  }
170
174
 
171
- async reserveEngineId(engineId: string): Promise<boolean> {
172
- const key = this.mintKey(KeyType.ENGINE_ID, { engineId });
173
- const success = await this.redisClient[this.commands.setnx](key, 'id', 1);
174
- return this.isSuccessful(success);
175
+ /**
176
+ * At any given time only a single engine will
177
+ * check for and process work items in the
178
+ * time and signal task queues.
179
+ */
180
+ async reserveScoutRole(scoutType: 'time' | 'signal', delay = SCOUT_INTERVAL_SECONDS): Promise<boolean> {
181
+ const key = this.mintKey(KeyType.WORK_ITEMS, { appId: this.appId, scoutType });
182
+ const success = await this.redisClient[this.commands.setnx](key, `${scoutType}:${formatISODate(new Date())}`);
183
+ if (this.isSuccessful(success)) {
184
+ await this.redisClient[this.commands.expire](key, delay - 1);
185
+ return true;
186
+ }
187
+ return false;
175
188
  }
176
189
 
177
190
  async getSettings(bCreate = false): Promise<HotMeshSettings> {
@@ -380,11 +393,11 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
380
393
  * list (added via RPUSH) are LPOPed. If origin was expired, then
381
394
  * LPOPed items from the list are likewise expired;
382
395
  */
383
- async setDependency(originJobId: string, topic: string, jobId: string, multi? : U): Promise<any> {
396
+ async setDependency(originJobId: string, topic: string, jobId: string, gId: string, multi? : U): Promise<any> {
384
397
  const privateMulti = multi || this.getMulti();
385
398
  const depParams = { appId: this.appId, jobId: originJobId };
386
399
  const depKey = this.mintKey(KeyType.JOB_DEPENDENTS, depParams);
387
- privateMulti[this.commands.rpush](depKey, `expire::${topic}::${jobId}`);
400
+ privateMulti[this.commands.rpush](depKey, `expire::${topic}::${gId}::${jobId}`);
388
401
  if (!multi) {
389
402
  return await privateMulti.exec();
390
403
  }
@@ -784,9 +797,9 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
784
797
  * for the given sleep group. Sleep groups are
785
798
  * organized into 'n'-second blocks (LISTS))
786
799
  */
787
- async registerTimeHook(jobId: string, activityId: string, type: 'sleep'|'expire'|'interrupt', deletionTime: number, multi?: U): Promise<void> {
800
+ async registerTimeHook(jobId: string, gId: string, activityId: string, type: 'sleep'|'expire'|'interrupt', deletionTime: number, multi?: U): Promise<void> {
788
801
  const listKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId, timeValue: deletionTime });
789
- const timeEvent = `${type}::${activityId}::${jobId}`
802
+ const timeEvent = `${type}::${activityId}::${gId}::${jobId}`;
790
803
  const len = await (multi || this.redisClient)[this.commands.rpush](listKey, timeEvent);
791
804
  if (multi || len === 1) {
792
805
  const zsetKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId });
@@ -794,7 +807,8 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
794
807
  }
795
808
  }
796
809
 
797
- async getNextTimeJob(listKey?: string): Promise<[listKey: string, jobId: string, activityId: string, type: 'sleep'|'expire'|'interrupt'] | void> {
810
+ async getNextTimeJob(listKey?: string): Promise<[listKey: string, jobId: string, gId: string, activityId: string, type: 'sleep'|'expire'|'interrupt'] | boolean> {
811
+ const existing = Boolean(listKey);
798
812
  const zsetKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId });
799
813
  listKey = listKey || await this.zRangeByScore(zsetKey, 0, Date.now());
800
814
  if (listKey) {
@@ -802,11 +816,12 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
802
816
  const timeEvent = await this.redisClient[this.commands.lpop](pKey);
803
817
  if (timeEvent) {
804
818
  //there are 3 time-related event triggers: sleep, expire, interrupt
805
- const [_type, activityId, ...jobId] = timeEvent.split('::');
806
- return [listKey, jobId.join('::'), activityId, pType];
819
+ const [_type, activityId, gId, ...jobId] = timeEvent.split('::');
820
+ return [listKey, jobId.join('::'), gId, activityId, pType];
807
821
  }
808
822
  await this.redisClient[this.commands.zrem](zsetKey, listKey);
809
823
  }
824
+ return existing;
810
825
  }
811
826
 
812
827
  /**
@@ -33,16 +33,16 @@ class IORedisStreamService extends StreamService<RedisClientType, RedisMultiType
33
33
  if (mkStream === 'MKSTREAM') {
34
34
  try {
35
35
  return (await this.redisClient.xgroup(command, key, groupName, id, mkStream)) === 'OK';
36
- } catch (err) {
36
+ } catch (error) {
37
37
  this.logger.info(`Consumer group not created with MKSTREAM for key: ${key} and group: ${groupName}`);
38
- throw err;
38
+ throw error;
39
39
  }
40
40
  } else {
41
41
  try {
42
42
  return (await this.redisClient.xgroup(command, key, groupName, id)) === 'OK';
43
- } catch (err) {
43
+ } catch (error) {
44
44
  this.logger.info(`Consumer group not created for key: ${key} and group: ${groupName}`);
45
- throw err;
45
+ throw error;
46
46
  }
47
47
  }
48
48
  }
@@ -50,9 +50,9 @@ class IORedisStreamService extends StreamService<RedisClientType, RedisMultiType
50
50
  async xadd(key: string, id: string, messageId: string, messageValue: string, multi?: RedisMultiType): Promise<string | RedisMultiType> {
51
51
  try {
52
52
  return await (multi || this.redisClient).xadd(key, id, messageId, messageValue);
53
- } catch (err) {
54
- this.logger.error(`Error publishing 'xadd'; key: ${key}`, err);
55
- throw err;
53
+ } catch (error) {
54
+ this.logger.error(`Error publishing 'xadd'; key: ${key}`, { error });
55
+ throw error;
56
56
  }
57
57
  }
58
58
 
@@ -79,9 +79,9 @@ class IORedisStreamService extends StreamService<RedisClientType, RedisMultiType
79
79
  streamName,
80
80
  id
81
81
  );
82
- } catch (err) {
83
- this.logger.error(`Error reading stream data [Stream ${streamName}] [Group ${groupName}]`, err);
84
- throw err;
82
+ } catch (error) {
83
+ this.logger.error(`Error reading stream data [Stream ${streamName}] [Group ${groupName}]`, { error });
84
+ throw error;
85
85
  }
86
86
  }
87
87
 
@@ -101,12 +101,12 @@ class IORedisStreamService extends StreamService<RedisClientType, RedisMultiType
101
101
  if (consumer) args.push(consumer);
102
102
  try {
103
103
  return await this.redisClient.call('XPENDING', ...args) as [string, string, number, number][];
104
- } catch (err) {
105
- this.logger.error('err, args', err, args);
104
+ } catch (error) {
105
+ this.logger.error('err, args', { error }, args);
106
106
  }
107
- } catch (err) {
108
- this.logger.error(`Error in retrieving pending messages for [stream ${key}], [group ${group}]`, err);
109
- throw err;
107
+ } catch (error) {
108
+ this.logger.error(`Error in retrieving pending messages for [stream ${key}], [group ${group}]`, { error });
109
+ throw error;
110
110
  }
111
111
  }
112
112
 
@@ -120,27 +120,36 @@ class IORedisStreamService extends StreamService<RedisClientType, RedisMultiType
120
120
  ): Promise<ReclaimedMessageType> {
121
121
  try {
122
122
  return await this.redisClient.xclaim(key, group, consumer, minIdleTime, id, ...args) as unknown as ReclaimedMessageType;
123
- } catch (err) {
124
- this.logger.error(`Error in claiming message with id: ${id} in group: ${group} for key: ${key}`, err);
125
- throw err;
123
+ } catch (error) {
124
+ this.logger.error(`Error in claiming message with id: ${id} in group: ${group} for key: ${key}`, { error });
125
+ throw error;
126
126
  }
127
127
  }
128
128
 
129
129
  async xack(key: string, group: string, id: string, multi? : RedisMultiType): Promise<number|RedisMultiType> {
130
130
  try {
131
131
  return await (multi || this.redisClient).xack(key, group, id);
132
- } catch (err) {
133
- this.logger.error(`Error in acknowledging messages in group: ${group} for key: ${key}`, err);
134
- throw err;
132
+ } catch (error) {
133
+ this.logger.error(`Error in acknowledging messages in group: ${group} for key: ${key}`, { error });
134
+ throw error;
135
135
  }
136
136
  }
137
137
 
138
138
  async xdel(key: string, id: string, multi? : RedisMultiType): Promise<number|RedisMultiType> {
139
139
  try {
140
140
  return await (multi || this.redisClient).xdel(key, id);
141
- } catch (err) {
142
- this.logger.error(`Error in deleting messages with id: ${id} for key: ${key}`, err);
143
- throw err;
141
+ } catch (error) {
142
+ this.logger.error(`Error in deleting messages with id: ${id} for key: ${key}`, { error });
143
+ throw error;
144
+ }
145
+ }
146
+
147
+ async xlen(key: string, multi? : RedisMultiType): Promise<number|RedisMultiType> {
148
+ try {
149
+ return await (multi || this.redisClient).xlen(key);
150
+ } catch (error) {
151
+ this.logger.error(`Error getting stream depth: ${key}`, { error });
152
+ throw error;
144
153
  }
145
154
  }
146
155
  }
@@ -139,6 +139,20 @@ class RedisStreamService extends StreamService<RedisClientType, RedisMultiType>
139
139
  throw err;
140
140
  }
141
141
  }
142
+
143
+ async xlen(key: string, multi? : RedisMultiType): Promise<number|RedisMultiType> {
144
+ try {
145
+ if (multi) {
146
+ multi.XLEN(key);
147
+ return multi;
148
+ } else {
149
+ return await this.redisClient.XLEN(key);
150
+ }
151
+ } catch (error) {
152
+ this.logger.error(`Error getting stream depth: ${key}`, { error });
153
+ throw error;
154
+ }
155
+ }
142
156
  }
143
157
 
144
158
  export { RedisStreamService };
@@ -52,6 +52,7 @@ abstract class StreamService<T, U> {
52
52
  ...args: string[]): Promise<ReclaimedMessageType>;
53
53
  abstract xack(key: string, group: string, id: string, multi?: U): Promise<number|U>;
54
54
  abstract xdel(key: string, id: string, multi?: U): Promise<number|U>;
55
+ abstract xlen(key: string, multi?: U): Promise<number|U>;
55
56
  }
56
57
 
57
58
  export { StreamService };
@@ -1,17 +1,20 @@
1
1
  import {
2
2
  EXPIRE_DURATION,
3
- FIDELITY_SECONDS } from '../../modules/enums';
3
+ FIDELITY_SECONDS,
4
+ SCOUT_INTERVAL_SECONDS} from '../../modules/enums';
4
5
  import { XSleepFor, sleepFor } from '../../modules/utils';
5
6
  import { ILogger } from '../logger';
6
7
  import { StoreService } from '../store';
7
- import { HookInterface } from '../../types/hook';
8
- import { JobCompletionOptions } from '../../types/job';
8
+ import { HookInterface, HookRule, HookSignal } from '../../types/hook';
9
+ import { JobCompletionOptions, JobState } from '../../types/job';
9
10
  import { RedisClient, RedisMulti } from '../../types/redis';
11
+ import { Pipe } from '../pipe';
10
12
 
11
13
  class TaskService {
12
14
  store: StoreService<RedisClient, RedisMulti>;
13
15
  logger: ILogger;
14
16
  cleanupTimeout: NodeJS.Timeout | null = null;
17
+ isScout: boolean = false;
15
18
 
16
19
  constructor(
17
20
  store: StoreService<RedisClient, RedisMulti>,
@@ -50,28 +53,59 @@ class TaskService {
50
53
  }
51
54
  }
52
55
 
53
- async registerTimeHook(jobId: string, activityId: string, type: 'sleep'|'expire'|'interrupt', inSeconds = FIDELITY_SECONDS, multi?: RedisMulti): Promise<void> {
56
+ async registerTimeHook(jobId: string, gId: string, activityId: string, type: 'sleep'|'expire'|'interrupt', inSeconds = FIDELITY_SECONDS, multi?: RedisMulti): Promise<void> {
54
57
  const awakenTimeSlot = Math.floor((Date.now() + (inSeconds * 1000)) / (FIDELITY_SECONDS * 1000)) * (FIDELITY_SECONDS * 1000); //n second awaken groups
55
- await this.store.registerTimeHook(jobId, activityId, type, awakenTimeSlot, multi);
58
+ await this.store.registerTimeHook(jobId, gId, activityId, type, awakenTimeSlot, multi);
56
59
  }
57
60
 
58
- async processTimeHooks(timeEventCallback: (jobId: string, activityId: string, type: 'sleep'|'expire'|'interrupt') => Promise<void>, listKey?: string): Promise<void> {
59
- try {
60
- const timeJob = await this.store.getNextTimeJob(listKey);
61
- if (timeJob) {
62
- const [listKey, jobId, activityId, type] = timeJob;
63
- await timeEventCallback(jobId, activityId, type);
64
- await sleepFor(0);
65
- this.processTimeHooks(timeEventCallback, listKey);
66
- } else {
67
- let sleep = XSleepFor(FIDELITY_SECONDS * 1000);
68
- this.cleanupTimeout = sleep.timerId;
69
- await sleep.promise;
70
- this.processTimeHooks(timeEventCallback)
61
+ /**
62
+ * Should this engine instance play the role of 'scout' for the quorum.
63
+ */
64
+ async shouldScout() {
65
+ const wasScout = this.isScout;
66
+ const isScout = wasScout || (this.isScout = await this.store.reserveScoutRole('time'));
67
+ if (isScout) {
68
+ if (!wasScout) {
69
+ setTimeout(() => {
70
+ this.isScout = false;
71
+ }, SCOUT_INTERVAL_SECONDS * 1_000);
72
+ }
73
+ return true;
74
+ }
75
+ return false;
76
+ }
77
+
78
+ async processTimeHooks(timeEventCallback: (jobId: string, gId: string, activityId: string, type: 'sleep'|'expire'|'interrupt') => Promise<void>, listKey?: string): Promise<void> {
79
+ if (await this.shouldScout()) {
80
+ try {
81
+ const timeJob = await this.store.getNextTimeJob(listKey);
82
+ if (Array.isArray(timeJob)) {
83
+ //a queue had a job; try again immediately
84
+ const [listKey, jobId, gId, activityId, type] = timeJob;
85
+ await timeEventCallback(jobId, gId, activityId, type);
86
+ await sleepFor(0);
87
+ this.processTimeHooks(timeEventCallback, listKey);
88
+ } else if (timeJob) {
89
+ //a queue was just emptied; try again immediately
90
+ await sleepFor(0);
91
+ this.processTimeHooks(timeEventCallback);
92
+ } else {
93
+ //all queues are empty; sleep before checking
94
+ let sleep = XSleepFor(FIDELITY_SECONDS * 1000);
95
+ this.cleanupTimeout = sleep.timerId;
96
+ await sleep.promise;
97
+ this.processTimeHooks(timeEventCallback);
98
+ }
99
+ } catch (err) {
100
+ //todo: retry connect to redis
101
+ this.logger.error('task-process-timehooks-error', err);
71
102
  }
72
- } catch (err) {
73
- //todo: retry connect to redis
74
- this.logger.error('task-process-timehooks-error', err);
103
+ } else {
104
+ //didn't get the scout role; try again in 'one-ish' minutes
105
+ let sleep = XSleepFor(SCOUT_INTERVAL_SECONDS * 1_000 * 2 * Math.random());
106
+ this.cleanupTimeout = sleep.timerId;
107
+ await sleep.promise;
108
+ this.processTimeHooks(timeEventCallback);
75
109
  }
76
110
  }
77
111
 
@@ -81,6 +115,71 @@ class TaskService {
81
115
  this.cleanupTimeout = undefined;
82
116
  }
83
117
  }
118
+
119
+ async getHookRule(topic: string): Promise<HookRule | undefined> {
120
+ const rules = await this.store.getHookRules();
121
+ return rules?.[topic]?.[0] as HookRule;
122
+ }
123
+
124
+ async registerWebHook(topic: string, context: JobState, dad: string, multi?: RedisMulti): Promise<string> {
125
+ const hookRule = await this.getHookRule(topic);
126
+ if (hookRule) {
127
+ const mapExpression = hookRule.conditions.match[0].expected;
128
+ const resolved = Pipe.resolve(mapExpression, context);
129
+ const jobId = context.metadata.jid;
130
+ const gId = context.metadata.gid;
131
+ const activityId = hookRule.to;
132
+ const hook: HookSignal = {
133
+ topic,
134
+ resolved,
135
+ jobId: `${activityId}::${dad}::${gId}::${jobId}`,
136
+ }
137
+ await this.store.setHookSignal(hook, multi);
138
+ return jobId;
139
+ } else {
140
+ throw new Error('signaler.registerWebHook:error: hook rule not found');
141
+ }
142
+ }
143
+
144
+ async processWebHookSignal(topic: string, data: Record<string, unknown>): Promise<[string, string, string, string] | undefined> {
145
+ const hookRule = await this.getHookRule(topic);
146
+ if (hookRule) {
147
+ //NOTE: both formats are supported by the mapping engine:
148
+ // `$self.hook.data` OR `$hook.data`
149
+ const context = { $self: { hook: { data }}, $hook: { data }};
150
+ const mapExpression = hookRule.conditions.match[0].actual;
151
+ const resolved = Pipe.resolve(mapExpression, context);
152
+ const hookSignalId = await this.store.getHookSignal(topic, resolved);
153
+ if (!hookSignalId) {
154
+ //messages can be double-processed; not an issue; return undefined
155
+ //users can also provide a bogus topic; not an issue; return undefined
156
+ return undefined;
157
+ }
158
+ //`aid` is part of composit key, but the hook `topic` is its public interface;
159
+ // this means that a new version of the graph can be deployed and the
160
+ // topic can be re-mapped to a different activity id. Outside callers
161
+ // can adhere to the unchanged contract (calling the same topic),
162
+ // while the internal system can be updated in real time as necessary.
163
+ const [_aid, dad, gid, ...jid] = hookSignalId.split('::');
164
+ return [jid.join('::'), hookRule.to, dad, gid];
165
+ } else {
166
+ throw new Error('signal-not-found');
167
+ }
168
+ }
169
+
170
+ async deleteWebHookSignal(topic: string, data: Record<string, unknown>): Promise<number> {
171
+ const hookRule = await this.getHookRule(topic);
172
+ if (hookRule) {
173
+ //NOTE: both formats are supported by the mapping engine:
174
+ // `$self.hook.data` OR `$hook.data`
175
+ const context = { $self: { hook: { data }}, $hook: { data }};
176
+ const mapExpression = hookRule.conditions.match[0].actual;
177
+ const resolved = Pipe.resolve(mapExpression, context);
178
+ return await this.store.deleteHookSignal(topic, resolved);
179
+ } else {
180
+ throw new Error('signaler.process:error: hook rule not found');
181
+ }
182
+ }
84
183
  }
85
184
 
86
185
  export { TaskService };
@@ -126,20 +126,20 @@ class TelemetryService {
126
126
  return result;
127
127
  }, {})
128
128
  };
129
- this.span.setAttributes(namespacedAtts as StringScalarType);
129
+ this.span?.setAttributes(namespacedAtts as StringScalarType);
130
130
  }
131
131
  }
132
132
 
133
133
  setActivityAttributes(attributes: StringScalarType): void {
134
- this.span.setAttributes(attributes);
134
+ this.span?.setAttributes(attributes);
135
135
  }
136
136
 
137
137
  setStreamAttributes(attributes: StringScalarType): void {
138
- this.span.setAttributes(attributes);
138
+ this.span?.setAttributes(attributes);
139
139
  }
140
140
 
141
141
  setJobAttributes(attributes: StringScalarType): void {
142
- this.jobSpan.setAttributes(attributes);
142
+ this.jobSpan?.setAttributes(attributes);
143
143
  }
144
144
 
145
145
  endJobSpan(): void {
@@ -216,11 +216,11 @@ class TelemetryService {
216
216
  }
217
217
 
218
218
  setActivityError(message: string) {
219
- this.span.setStatus({ code: SpanStatusCode.ERROR, message });
219
+ this.span?.setStatus({ code: SpanStatusCode.ERROR, message });
220
220
  }
221
221
 
222
222
  setStreamError(message: string) {
223
- this.span.setStatus({ code: SpanStatusCode.ERROR, message });
223
+ this.span?.setStatus({ code: SpanStatusCode.ERROR, message });
224
224
  }
225
225
 
226
226
  /**
@@ -1,6 +1,6 @@
1
1
  import { KeyType } from "../../modules/key";
2
2
  import { ILogger } from "../logger";
3
- import { StreamSignaler } from "../signaler/stream";
3
+ import { Router } from "../router";
4
4
  import { StoreService } from '../store';
5
5
  import { RedisStoreService as RedisStore } from '../store/clients/redis';
6
6
  import { IORedisStoreService as IORedisStore } from '../store/clients/ioredis';
@@ -14,6 +14,7 @@ import { RedisClientType as IORedisClientType } from '../../types/ioredisclient'
14
14
  import { HotMeshConfig, HotMeshWorker } from "../../types/hotmesh";
15
15
  import {
16
16
  QuorumMessage,
17
+ QuorumProfile,
17
18
  SubscriptionCallback } from "../../types/quorum";
18
19
  import { RedisClient, RedisMulti } from "../../types/redis";
19
20
  import { RedisClientType } from '../../types/redisclient';
@@ -30,7 +31,7 @@ class WorkerService {
30
31
  store: StoreService<RedisClient, RedisMulti> | null;
31
32
  stream: StreamService<RedisClient, RedisMulti> | null;
32
33
  subscribe: SubService<RedisClient, RedisMulti> | null;
33
- streamSignaler: StreamSignaler | null;
34
+ router: Router | null;
34
35
  logger: ILogger;
35
36
  reporting = false;
36
37
 
@@ -66,10 +67,10 @@ class WorkerService {
66
67
  await service.subscribe.subscribe(KeyType.QUORUM, service.subscriptionHandler(), appId, service.topic);
67
68
  await service.subscribe.subscribe(KeyType.QUORUM, service.subscriptionHandler(), appId, service.guid);
68
69
  await service.initStreamChannel(service, worker.stream);
69
- service.streamSignaler = service.initStreamSignaler(worker, logger);
70
+ service.router = service.initRouter(worker, logger);
70
71
 
71
72
  const key = service.stream.mintKey(KeyType.STREAMS, { appId: service.appId, topic: worker.topic });
72
- await service.streamSignaler.consumeMessages(
73
+ await service.router.consumeMessages(
73
74
  key,
74
75
  'WORKER',
75
76
  service.guid,
@@ -130,8 +131,8 @@ class WorkerService {
130
131
  );
131
132
  }
132
133
 
133
- initStreamSignaler(worker: HotMeshWorker, logger: ILogger): StreamSignaler {
134
- return new StreamSignaler(
134
+ initRouter(worker: HotMeshWorker, logger: ILogger): Router {
135
+ return new Router(
135
136
  {
136
137
  namespace: this.namespace,
137
138
  appId: this.appId,
@@ -153,12 +154,41 @@ class WorkerService {
153
154
  self.logger.debug('worker-event-received', { topic, type: message.type });
154
155
  if (message.type === 'throttle') {
155
156
  self.throttle(message.throttle);
157
+ } else if(message.type === 'ping') {
158
+ self.sayPong(self.appId, self.guid, message.originator, message.details);
156
159
  }
157
160
  };
158
161
  }
159
162
 
163
+ async sayPong(appId: string, guid: string, originator: string, details = false) {
164
+ let profile: QuorumProfile;
165
+ if (details) {
166
+ const params = {
167
+ appId: this.appId,
168
+ topic: this.topic,
169
+ };
170
+
171
+ profile = {
172
+ engine_id: this.guid,
173
+ namespace: this.namespace,
174
+ app_id: this.appId,
175
+ worker_topic: this.topic,
176
+ stream: this.stream.mintKey(KeyType.STREAMS, params),
177
+ };
178
+ }
179
+ this.store.publish(
180
+ KeyType.QUORUM,
181
+ {
182
+ type: 'pong',
183
+ guid, originator,
184
+ profile,
185
+ },
186
+ appId,
187
+ );
188
+ }
189
+
160
190
  async throttle(delayInMillis: number) {
161
- this.streamSignaler.setThrottle(delayInMillis);
191
+ this.router.setThrottle(delayInMillis);
162
192
  }
163
193
  }
164
194
 
package/types/job.ts CHANGED
@@ -8,10 +8,12 @@ type ActivityData = {
8
8
 
9
9
  type JobMetadata = {
10
10
  key?: string; //job_key
11
+ gid: string; //system assigned guid; ensured created/deleted/created jobs are unique
11
12
  jid: string; //job_id (jid+dad+aid) is composite key for activity
12
13
  dad: string; //dimensional address for the activity (,0,0,1)
13
14
  aid: string; //activity_id as in the YAML file
14
15
  pj?: string; //parent_job_id (pj+pd+pa) is composite key for parent activity
16
+ pg?: string; //parent_generational_id (system assigned at trigger inception); pg is the parent job's gid (just in case user created/deleted/created a job with same jid)
15
17
  pd?: string; //parent_dimensional_address
16
18
  pa?: string; //parent_activity_id
17
19
  ngn?: string; //engine guid (one time subscriptions)
package/types/quorum.ts CHANGED
@@ -1,9 +1,20 @@
1
1
  import { JobOutput } from "./job";
2
2
 
3
- //used for coordination like version activation
3
+ //used for coordination (like version activation)
4
+
5
+ export interface QuorumProfile {
6
+ namespace: string;
7
+ app_id: string;
8
+ engine_id: string;
9
+ worker_topic?: string;
10
+ stream?: string;
11
+ stream_depth?: number;
12
+ }
13
+
4
14
  export interface PingMessage {
5
15
  type: 'ping';
6
16
  originator: string; //guid
17
+ details?: boolean; //if true, all endpoints will include their profile
7
18
  }
8
19
 
9
20
  export interface WorkMessage {
@@ -16,11 +27,11 @@ export interface CronMessage {
16
27
  originator: string; //guid
17
28
  }
18
29
 
19
- //used for coordination like version activation
20
30
  export interface PongMessage {
21
31
  type: 'pong';
22
- originator: string; //clone of originator guid passed in ping
23
- guid: string;
32
+ guid: string; //call initiator
33
+ originator: string; //clone of originator guid passed in ping
34
+ profile?: QuorumProfile; //contains details about the engine/worker
24
35
  }
25
36
 
26
37
  export interface ActivateMessage {
@@ -6,6 +6,7 @@ interface RedisMultiType {
6
6
  XADD(key: string, id: string, fields: any): this;
7
7
  XACK(key: string, group: string, id: string): this;
8
8
  XDEL(key: string, id: string): this;
9
+ XLEN(key: string): this;
9
10
  HDEL(key: string, itemId: string): this;
10
11
  HGET(key: string, itemId: string): this;
11
12
  HGETALL(key: string): this;
package/types/stream.ts CHANGED
@@ -35,12 +35,13 @@ export interface StreamData {
35
35
  metadata: {
36
36
  guid: string; //every message is minted with a guid to distinguish retries from new messages
37
37
  topic?: string;
38
- jid?: string; //is optonal if type is WEBHOOK
39
- dad?: string; //dimensional address
38
+ jid?: string; //is optional if type is WEBHOOK (system assigned or user assigned)
39
+ gid?: string; //is optional if type is WEBHOOK (system assigned job guid)
40
+ dad?: string; //dimensional address
40
41
  aid: string;
41
- trc?: string; //trace id
42
- spn?: string; //span id
43
- try?: number; //current try count
42
+ trc?: string; //trace id
43
+ spn?: string; //span id
44
+ try?: number; //current try count
44
45
  };
45
46
  type?: StreamDataType;
46
47
  data: Record<string, unknown>;
@@ -1,15 +0,0 @@
1
- import { ILogger } from '../logger';
2
- import { StoreService } from '../store';
3
- import { HookRule } from '../../types/hook';
4
- import { JobState } from '../../types/job';
5
- import { RedisClient, RedisMulti } from '../../types/redis';
6
- declare class StoreSignaler {
7
- store: StoreService<RedisClient, RedisMulti>;
8
- logger: ILogger;
9
- constructor(store: StoreService<RedisClient, RedisMulti>, logger: ILogger);
10
- getHookRule(topic: string): Promise<HookRule | undefined>;
11
- registerWebHook(topic: string, context: JobState, dad: string, multi?: RedisMulti): Promise<string>;
12
- processWebHookSignal(topic: string, data: Record<string, unknown>): Promise<[string, string, string] | undefined>;
13
- deleteWebHookSignal(topic: string, data: Record<string, unknown>): Promise<number>;
14
- }
15
- export { StoreSignaler };