@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
@@ -99,10 +99,19 @@ class StoreService {
99
99
  invalidateCache() {
100
100
  this.cache.invalidate();
101
101
  }
102
- async reserveEngineId(engineId) {
103
- const key = this.mintKey(key_1.KeyType.ENGINE_ID, { engineId });
104
- const success = await this.redisClient[this.commands.setnx](key, 'id', 1);
105
- return this.isSuccessful(success);
102
+ /**
103
+ * At any given time only a single engine will
104
+ * check for and process work items in the
105
+ * time and signal task queues.
106
+ */
107
+ async reserveScoutRole(scoutType, delay = enums_1.SCOUT_INTERVAL_SECONDS) {
108
+ const key = this.mintKey(key_1.KeyType.WORK_ITEMS, { appId: this.appId, scoutType });
109
+ const success = await this.redisClient[this.commands.setnx](key, `${scoutType}:${(0, utils_1.formatISODate)(new Date())}`);
110
+ if (this.isSuccessful(success)) {
111
+ await this.redisClient[this.commands.expire](key, delay - 1);
112
+ return true;
113
+ }
114
+ return false;
106
115
  }
107
116
  async getSettings(bCreate = false) {
108
117
  let settings = this.cache?.getSettings();
@@ -304,11 +313,11 @@ class StoreService {
304
313
  * list (added via RPUSH) are LPOPed. If origin was expired, then
305
314
  * LPOPed items from the list are likewise expired;
306
315
  */
307
- async setDependency(originJobId, topic, jobId, multi) {
316
+ async setDependency(originJobId, topic, jobId, gId, multi) {
308
317
  const privateMulti = multi || this.getMulti();
309
318
  const depParams = { appId: this.appId, jobId: originJobId };
310
319
  const depKey = this.mintKey(key_1.KeyType.JOB_DEPENDENTS, depParams);
311
- privateMulti[this.commands.rpush](depKey, `expire::${topic}::${jobId}`);
320
+ privateMulti[this.commands.rpush](depKey, `expire::${topic}::${gId}::${jobId}`);
312
321
  if (!multi) {
313
322
  return await privateMulti.exec();
314
323
  }
@@ -682,9 +691,9 @@ class StoreService {
682
691
  * for the given sleep group. Sleep groups are
683
692
  * organized into 'n'-second blocks (LISTS))
684
693
  */
685
- async registerTimeHook(jobId, activityId, type, deletionTime, multi) {
694
+ async registerTimeHook(jobId, gId, activityId, type, deletionTime, multi) {
686
695
  const listKey = this.mintKey(key_1.KeyType.TIME_RANGE, { appId: this.appId, timeValue: deletionTime });
687
- const timeEvent = `${type}::${activityId}::${jobId}`;
696
+ const timeEvent = `${type}::${activityId}::${gId}::${jobId}`;
688
697
  const len = await (multi || this.redisClient)[this.commands.rpush](listKey, timeEvent);
689
698
  if (multi || len === 1) {
690
699
  const zsetKey = this.mintKey(key_1.KeyType.TIME_RANGE, { appId: this.appId });
@@ -692,6 +701,7 @@ class StoreService {
692
701
  }
693
702
  }
694
703
  async getNextTimeJob(listKey) {
704
+ const existing = Boolean(listKey);
695
705
  const zsetKey = this.mintKey(key_1.KeyType.TIME_RANGE, { appId: this.appId });
696
706
  listKey = listKey || await this.zRangeByScore(zsetKey, 0, Date.now());
697
707
  if (listKey) {
@@ -699,11 +709,12 @@ class StoreService {
699
709
  const timeEvent = await this.redisClient[this.commands.lpop](pKey);
700
710
  if (timeEvent) {
701
711
  //there are 3 time-related event triggers: sleep, expire, interrupt
702
- const [_type, activityId, ...jobId] = timeEvent.split('::');
703
- return [listKey, jobId.join('::'), activityId, pType];
712
+ const [_type, activityId, gId, ...jobId] = timeEvent.split('::');
713
+ return [listKey, jobId.join('::'), gId, activityId, pType];
704
714
  }
705
715
  await this.redisClient[this.commands.zrem](zsetKey, listKey);
706
716
  }
717
+ return existing;
707
718
  }
708
719
  /**
709
720
  * when processing time jobs, the target LIST ID returned
@@ -19,5 +19,6 @@ declare class IORedisStreamService extends StreamService<RedisClientType, RedisM
19
19
  xclaim(key: string, group: string, consumer: string, minIdleTime: number, id: string, ...args: string[]): Promise<ReclaimedMessageType>;
20
20
  xack(key: string, group: string, id: string, multi?: RedisMultiType): Promise<number | RedisMultiType>;
21
21
  xdel(key: string, id: string, multi?: RedisMultiType): Promise<number | RedisMultiType>;
22
+ xlen(key: string, multi?: RedisMultiType): Promise<number | RedisMultiType>;
22
23
  }
23
24
  export { IORedisStreamService };
@@ -25,18 +25,18 @@ class IORedisStreamService extends index_1.StreamService {
25
25
  try {
26
26
  return (await this.redisClient.xgroup(command, key, groupName, id, mkStream)) === 'OK';
27
27
  }
28
- catch (err) {
28
+ catch (error) {
29
29
  this.logger.info(`Consumer group not created with MKSTREAM for key: ${key} and group: ${groupName}`);
30
- throw err;
30
+ throw error;
31
31
  }
32
32
  }
33
33
  else {
34
34
  try {
35
35
  return (await this.redisClient.xgroup(command, key, groupName, id)) === 'OK';
36
36
  }
37
- catch (err) {
37
+ catch (error) {
38
38
  this.logger.info(`Consumer group not created for key: ${key} and group: ${groupName}`);
39
- throw err;
39
+ throw error;
40
40
  }
41
41
  }
42
42
  }
@@ -44,9 +44,9 @@ class IORedisStreamService extends index_1.StreamService {
44
44
  try {
45
45
  return await (multi || this.redisClient).xadd(key, id, messageId, messageValue);
46
46
  }
47
- catch (err) {
48
- this.logger.error(`Error publishing 'xadd'; key: ${key}`, err);
49
- throw err;
47
+ catch (error) {
48
+ this.logger.error(`Error publishing 'xadd'; key: ${key}`, { error });
49
+ throw error;
50
50
  }
51
51
  }
52
52
  async xreadgroup(command, groupName, consumerName, blockOption, blockTime, streamsOption, streamName, id) {
@@ -56,9 +56,9 @@ class IORedisStreamService extends index_1.StreamService {
56
56
  // @ts-ignore
57
57
  blockOption, blockTime, streamsOption, streamName, id);
58
58
  }
59
- catch (err) {
60
- this.logger.error(`Error reading stream data [Stream ${streamName}] [Group ${groupName}]`, err);
61
- throw err;
59
+ catch (error) {
60
+ this.logger.error(`Error reading stream data [Stream ${streamName}] [Group ${groupName}]`, { error });
61
+ throw error;
62
62
  }
63
63
  }
64
64
  async xpending(key, group, start, end, count, consumer) {
@@ -75,40 +75,49 @@ class IORedisStreamService extends index_1.StreamService {
75
75
  try {
76
76
  return await this.redisClient.call('XPENDING', ...args);
77
77
  }
78
- catch (err) {
79
- this.logger.error('err, args', err, args);
78
+ catch (error) {
79
+ this.logger.error('err, args', { error }, args);
80
80
  }
81
81
  }
82
- catch (err) {
83
- this.logger.error(`Error in retrieving pending messages for [stream ${key}], [group ${group}]`, err);
84
- throw err;
82
+ catch (error) {
83
+ this.logger.error(`Error in retrieving pending messages for [stream ${key}], [group ${group}]`, { error });
84
+ throw error;
85
85
  }
86
86
  }
87
87
  async xclaim(key, group, consumer, minIdleTime, id, ...args) {
88
88
  try {
89
89
  return await this.redisClient.xclaim(key, group, consumer, minIdleTime, id, ...args);
90
90
  }
91
- catch (err) {
92
- this.logger.error(`Error in claiming message with id: ${id} in group: ${group} for key: ${key}`, err);
93
- throw err;
91
+ catch (error) {
92
+ this.logger.error(`Error in claiming message with id: ${id} in group: ${group} for key: ${key}`, { error });
93
+ throw error;
94
94
  }
95
95
  }
96
96
  async xack(key, group, id, multi) {
97
97
  try {
98
98
  return await (multi || this.redisClient).xack(key, group, id);
99
99
  }
100
- catch (err) {
101
- this.logger.error(`Error in acknowledging messages in group: ${group} for key: ${key}`, err);
102
- throw err;
100
+ catch (error) {
101
+ this.logger.error(`Error in acknowledging messages in group: ${group} for key: ${key}`, { error });
102
+ throw error;
103
103
  }
104
104
  }
105
105
  async xdel(key, id, multi) {
106
106
  try {
107
107
  return await (multi || this.redisClient).xdel(key, id);
108
108
  }
109
- catch (err) {
110
- this.logger.error(`Error in deleting messages with id: ${id} for key: ${key}`, err);
111
- throw err;
109
+ catch (error) {
110
+ this.logger.error(`Error in deleting messages with id: ${id} for key: ${key}`, { error });
111
+ throw error;
112
+ }
113
+ }
114
+ async xlen(key, multi) {
115
+ try {
116
+ return await (multi || this.redisClient).xlen(key);
117
+ }
118
+ catch (error) {
119
+ this.logger.error(`Error getting stream depth: ${key}`, { error });
120
+ throw error;
112
121
  }
113
122
  }
114
123
  }
@@ -19,5 +19,6 @@ declare class RedisStreamService extends StreamService<RedisClientType, RedisMul
19
19
  xclaim(key: string, group: string, consumer: string, minIdleTime: number, id: string, ...args: string[]): Promise<ReclaimedMessageType>;
20
20
  xack(key: string, group: string, id: string, multi?: RedisMultiType): Promise<number | RedisMultiType>;
21
21
  xdel(key: string, id: string, multi?: RedisMultiType): Promise<number | RedisMultiType>;
22
+ xlen(key: string, multi?: RedisMultiType): Promise<number | RedisMultiType>;
22
23
  }
23
24
  export { RedisStreamService };
@@ -115,5 +115,20 @@ class RedisStreamService extends index_1.StreamService {
115
115
  throw err;
116
116
  }
117
117
  }
118
+ async xlen(key, multi) {
119
+ try {
120
+ if (multi) {
121
+ multi.XLEN(key);
122
+ return multi;
123
+ }
124
+ else {
125
+ return await this.redisClient.XLEN(key);
126
+ }
127
+ }
128
+ catch (error) {
129
+ this.logger.error(`Error getting stream depth: ${key}`, { error });
130
+ throw error;
131
+ }
132
+ }
118
133
  }
119
134
  exports.RedisStreamService = RedisStreamService;
@@ -17,5 +17,6 @@ declare abstract class StreamService<T, U> {
17
17
  abstract xclaim(key: string, group: string, consumer: string, minIdleTime: number, id: string, ...args: string[]): Promise<ReclaimedMessageType>;
18
18
  abstract xack(key: string, group: string, id: string, multi?: U): Promise<number | U>;
19
19
  abstract xdel(key: string, id: string, multi?: U): Promise<number | U>;
20
+ abstract xlen(key: string, multi?: U): Promise<number | U>;
20
21
  }
21
22
  export { StreamService };
@@ -1,19 +1,28 @@
1
1
  /// <reference types="node" />
2
2
  import { ILogger } from '../logger';
3
3
  import { StoreService } from '../store';
4
- import { HookInterface } from '../../types/hook';
5
- import { JobCompletionOptions } from '../../types/job';
4
+ import { HookInterface, HookRule } from '../../types/hook';
5
+ import { JobCompletionOptions, JobState } from '../../types/job';
6
6
  import { RedisClient, RedisMulti } from '../../types/redis';
7
7
  declare class TaskService {
8
8
  store: StoreService<RedisClient, RedisMulti>;
9
9
  logger: ILogger;
10
10
  cleanupTimeout: NodeJS.Timeout | null;
11
+ isScout: boolean;
11
12
  constructor(store: StoreService<RedisClient, RedisMulti>, logger: ILogger);
12
13
  processWebHooks(hookEventCallback: HookInterface): Promise<void>;
13
14
  enqueueWorkItems(keys: string[]): Promise<void>;
14
15
  registerJobForCleanup(jobId: string, inSeconds: number, options: JobCompletionOptions): Promise<void>;
15
- registerTimeHook(jobId: string, activityId: string, type: 'sleep' | 'expire' | 'interrupt', inSeconds?: number, multi?: RedisMulti): Promise<void>;
16
- processTimeHooks(timeEventCallback: (jobId: string, activityId: string, type: 'sleep' | 'expire' | 'interrupt') => Promise<void>, listKey?: string): Promise<void>;
16
+ registerTimeHook(jobId: string, gId: string, activityId: string, type: 'sleep' | 'expire' | 'interrupt', inSeconds?: number, multi?: RedisMulti): Promise<void>;
17
+ /**
18
+ * Should this engine instance play the role of 'scout' for the quorum.
19
+ */
20
+ shouldScout(): Promise<boolean>;
21
+ processTimeHooks(timeEventCallback: (jobId: string, gId: string, activityId: string, type: 'sleep' | 'expire' | 'interrupt') => Promise<void>, listKey?: string): Promise<void>;
17
22
  cancelCleanup(): void;
23
+ getHookRule(topic: string): Promise<HookRule | undefined>;
24
+ registerWebHook(topic: string, context: JobState, dad: string, multi?: RedisMulti): Promise<string>;
25
+ processWebHookSignal(topic: string, data: Record<string, unknown>): Promise<[string, string, string, string] | undefined>;
26
+ deleteWebHookSignal(topic: string, data: Record<string, unknown>): Promise<number>;
18
27
  }
19
28
  export { TaskService };
@@ -3,9 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TaskService = void 0;
4
4
  const enums_1 = require("../../modules/enums");
5
5
  const utils_1 = require("../../modules/utils");
6
+ const pipe_1 = require("../pipe");
6
7
  class TaskService {
7
8
  constructor(store, logger) {
8
9
  this.cleanupTimeout = null;
10
+ this.isScout = false;
9
11
  this.logger = logger;
10
12
  this.store = store;
11
13
  }
@@ -36,29 +38,61 @@ class TaskService {
36
38
  await this.store.registerExpireJob(jobId, expireTimeSlot, options);
37
39
  }
38
40
  }
39
- async registerTimeHook(jobId, activityId, type, inSeconds = enums_1.FIDELITY_SECONDS, multi) {
41
+ async registerTimeHook(jobId, gId, activityId, type, inSeconds = enums_1.FIDELITY_SECONDS, multi) {
40
42
  const awakenTimeSlot = Math.floor((Date.now() + (inSeconds * 1000)) / (enums_1.FIDELITY_SECONDS * 1000)) * (enums_1.FIDELITY_SECONDS * 1000); //n second awaken groups
41
- await this.store.registerTimeHook(jobId, activityId, type, awakenTimeSlot, multi);
43
+ await this.store.registerTimeHook(jobId, gId, activityId, type, awakenTimeSlot, multi);
44
+ }
45
+ /**
46
+ * Should this engine instance play the role of 'scout' for the quorum.
47
+ */
48
+ async shouldScout() {
49
+ const wasScout = this.isScout;
50
+ const isScout = wasScout || (this.isScout = await this.store.reserveScoutRole('time'));
51
+ if (isScout) {
52
+ if (!wasScout) {
53
+ setTimeout(() => {
54
+ this.isScout = false;
55
+ }, enums_1.SCOUT_INTERVAL_SECONDS * 1000);
56
+ }
57
+ return true;
58
+ }
59
+ return false;
42
60
  }
43
61
  async processTimeHooks(timeEventCallback, listKey) {
44
- try {
45
- const timeJob = await this.store.getNextTimeJob(listKey);
46
- if (timeJob) {
47
- const [listKey, jobId, activityId, type] = timeJob;
48
- await timeEventCallback(jobId, activityId, type);
49
- await (0, utils_1.sleepFor)(0);
50
- this.processTimeHooks(timeEventCallback, listKey);
62
+ if (await this.shouldScout()) {
63
+ try {
64
+ const timeJob = await this.store.getNextTimeJob(listKey);
65
+ if (Array.isArray(timeJob)) {
66
+ //a queue had a job; try again immediately
67
+ const [listKey, jobId, gId, activityId, type] = timeJob;
68
+ await timeEventCallback(jobId, gId, activityId, type);
69
+ await (0, utils_1.sleepFor)(0);
70
+ this.processTimeHooks(timeEventCallback, listKey);
71
+ }
72
+ else if (timeJob) {
73
+ //a queue was just emptied; try again immediately
74
+ await (0, utils_1.sleepFor)(0);
75
+ this.processTimeHooks(timeEventCallback);
76
+ }
77
+ else {
78
+ //all queues are empty; sleep before checking
79
+ let sleep = (0, utils_1.XSleepFor)(enums_1.FIDELITY_SECONDS * 1000);
80
+ this.cleanupTimeout = sleep.timerId;
81
+ await sleep.promise;
82
+ this.processTimeHooks(timeEventCallback);
83
+ }
51
84
  }
52
- else {
53
- let sleep = (0, utils_1.XSleepFor)(enums_1.FIDELITY_SECONDS * 1000);
54
- this.cleanupTimeout = sleep.timerId;
55
- await sleep.promise;
56
- this.processTimeHooks(timeEventCallback);
85
+ catch (err) {
86
+ //todo: retry connect to redis
87
+ this.logger.error('task-process-timehooks-error', err);
57
88
  }
58
89
  }
59
- catch (err) {
60
- //todo: retry connect to redis
61
- this.logger.error('task-process-timehooks-error', err);
90
+ else {
91
+ //didn't get the scout role; try again in 'one-ish' minutes
92
+ let sleep = (0, utils_1.XSleepFor)(enums_1.SCOUT_INTERVAL_SECONDS * 1000 * 2 * Math.random());
93
+ this.cleanupTimeout = sleep.timerId;
94
+ await sleep.promise;
95
+ this.processTimeHooks(timeEventCallback);
62
96
  }
63
97
  }
64
98
  cancelCleanup() {
@@ -67,5 +101,69 @@ class TaskService {
67
101
  this.cleanupTimeout = undefined;
68
102
  }
69
103
  }
104
+ async getHookRule(topic) {
105
+ const rules = await this.store.getHookRules();
106
+ return rules?.[topic]?.[0];
107
+ }
108
+ async registerWebHook(topic, context, dad, multi) {
109
+ const hookRule = await this.getHookRule(topic);
110
+ if (hookRule) {
111
+ const mapExpression = hookRule.conditions.match[0].expected;
112
+ const resolved = pipe_1.Pipe.resolve(mapExpression, context);
113
+ const jobId = context.metadata.jid;
114
+ const gId = context.metadata.gid;
115
+ const activityId = hookRule.to;
116
+ const hook = {
117
+ topic,
118
+ resolved,
119
+ jobId: `${activityId}::${dad}::${gId}::${jobId}`,
120
+ };
121
+ await this.store.setHookSignal(hook, multi);
122
+ return jobId;
123
+ }
124
+ else {
125
+ throw new Error('signaler.registerWebHook:error: hook rule not found');
126
+ }
127
+ }
128
+ async processWebHookSignal(topic, data) {
129
+ const hookRule = await this.getHookRule(topic);
130
+ if (hookRule) {
131
+ //NOTE: both formats are supported by the mapping engine:
132
+ // `$self.hook.data` OR `$hook.data`
133
+ const context = { $self: { hook: { data } }, $hook: { data } };
134
+ const mapExpression = hookRule.conditions.match[0].actual;
135
+ const resolved = pipe_1.Pipe.resolve(mapExpression, context);
136
+ const hookSignalId = await this.store.getHookSignal(topic, resolved);
137
+ if (!hookSignalId) {
138
+ //messages can be double-processed; not an issue; return undefined
139
+ //users can also provide a bogus topic; not an issue; return undefined
140
+ return undefined;
141
+ }
142
+ //`aid` is part of composit key, but the hook `topic` is its public interface;
143
+ // this means that a new version of the graph can be deployed and the
144
+ // topic can be re-mapped to a different activity id. Outside callers
145
+ // can adhere to the unchanged contract (calling the same topic),
146
+ // while the internal system can be updated in real time as necessary.
147
+ const [_aid, dad, gid, ...jid] = hookSignalId.split('::');
148
+ return [jid.join('::'), hookRule.to, dad, gid];
149
+ }
150
+ else {
151
+ throw new Error('signal-not-found');
152
+ }
153
+ }
154
+ async deleteWebHookSignal(topic, data) {
155
+ const hookRule = await this.getHookRule(topic);
156
+ if (hookRule) {
157
+ //NOTE: both formats are supported by the mapping engine:
158
+ // `$self.hook.data` OR `$hook.data`
159
+ const context = { $self: { hook: { data } }, $hook: { data } };
160
+ const mapExpression = hookRule.conditions.match[0].actual;
161
+ const resolved = pipe_1.Pipe.resolve(mapExpression, context);
162
+ return await this.store.deleteHookSignal(topic, resolved);
163
+ }
164
+ else {
165
+ throw new Error('signaler.process:error: hook rule not found');
166
+ }
167
+ }
70
168
  }
71
169
  exports.TaskService = TaskService;
@@ -93,17 +93,17 @@ class TelemetryService {
93
93
  return result;
94
94
  }, {})
95
95
  };
96
- this.span.setAttributes(namespacedAtts);
96
+ this.span?.setAttributes(namespacedAtts);
97
97
  }
98
98
  }
99
99
  setActivityAttributes(attributes) {
100
- this.span.setAttributes(attributes);
100
+ this.span?.setAttributes(attributes);
101
101
  }
102
102
  setStreamAttributes(attributes) {
103
- this.span.setAttributes(attributes);
103
+ this.span?.setAttributes(attributes);
104
104
  }
105
105
  setJobAttributes(attributes) {
106
- this.jobSpan.setAttributes(attributes);
106
+ this.jobSpan?.setAttributes(attributes);
107
107
  }
108
108
  endJobSpan() {
109
109
  this.endSpan(this.jobSpan);
@@ -174,10 +174,10 @@ class TelemetryService {
174
174
  }
175
175
  }
176
176
  setActivityError(message) {
177
- this.span.setStatus({ code: telemetry_1.SpanStatusCode.ERROR, message });
177
+ this.span?.setStatus({ code: telemetry_1.SpanStatusCode.ERROR, message });
178
178
  }
179
179
  setStreamError(message) {
180
- this.span.setStatus({ code: telemetry_1.SpanStatusCode.ERROR, message });
180
+ this.span?.setStatus({ code: telemetry_1.SpanStatusCode.ERROR, message });
181
181
  }
182
182
  /**
183
183
  * Adds the paths (HGET) necessary to restore telemetry state for an activity
@@ -1,5 +1,5 @@
1
1
  import { ILogger } from "../logger";
2
- import { StreamSignaler } from "../signaler/stream";
2
+ import { Router } from "../router";
3
3
  import { StoreService } from '../store';
4
4
  import { StreamService } from '../stream';
5
5
  import { SubService } from '../sub';
@@ -15,7 +15,7 @@ declare class WorkerService {
15
15
  store: StoreService<RedisClient, RedisMulti> | null;
16
16
  stream: StreamService<RedisClient, RedisMulti> | null;
17
17
  subscribe: SubService<RedisClient, RedisMulti> | null;
18
- streamSignaler: StreamSignaler | null;
18
+ router: Router | null;
19
19
  logger: ILogger;
20
20
  reporting: boolean;
21
21
  static init(namespace: string, appId: string, guid: string, config: HotMeshConfig, logger: ILogger): Promise<WorkerService[]>;
@@ -23,8 +23,9 @@ declare class WorkerService {
23
23
  initStoreChannel(service: WorkerService, store: RedisClient): Promise<void>;
24
24
  initSubChannel(service: WorkerService, sub: RedisClient): Promise<void>;
25
25
  initStreamChannel(service: WorkerService, stream: RedisClient): Promise<void>;
26
- initStreamSignaler(worker: HotMeshWorker, logger: ILogger): StreamSignaler;
26
+ initRouter(worker: HotMeshWorker, logger: ILogger): Router;
27
27
  subscriptionHandler(): SubscriptionCallback;
28
+ sayPong(appId: string, guid: string, originator: string, details?: boolean): Promise<void>;
28
29
  throttle(delayInMillis: number): Promise<void>;
29
30
  }
30
31
  export { WorkerService };
@@ -2,14 +2,14 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WorkerService = void 0;
4
4
  const key_1 = require("../../modules/key");
5
- const stream_1 = require("../signaler/stream");
5
+ const router_1 = require("../router");
6
6
  const redis_1 = require("../store/clients/redis");
7
7
  const ioredis_1 = require("../store/clients/ioredis");
8
8
  const redis_2 = require("../stream/clients/redis");
9
9
  const ioredis_2 = require("../stream/clients/ioredis");
10
10
  const ioredis_3 = require("../sub/clients/ioredis");
11
11
  const redis_3 = require("../sub/clients/redis");
12
- const stream_2 = require("../../types/stream");
12
+ const stream_1 = require("../../types/stream");
13
13
  const utils_1 = require("../../modules/utils");
14
14
  const connector_1 = require("../connector");
15
15
  class WorkerService {
@@ -35,9 +35,9 @@ class WorkerService {
35
35
  await service.subscribe.subscribe(key_1.KeyType.QUORUM, service.subscriptionHandler(), appId, service.topic);
36
36
  await service.subscribe.subscribe(key_1.KeyType.QUORUM, service.subscriptionHandler(), appId, service.guid);
37
37
  await service.initStreamChannel(service, worker.stream);
38
- service.streamSignaler = service.initStreamSignaler(worker, logger);
38
+ service.router = service.initRouter(worker, logger);
39
39
  const key = service.stream.mintKey(key_1.KeyType.STREAMS, { appId: service.appId, topic: worker.topic });
40
- await service.streamSignaler.consumeMessages(key, 'WORKER', service.guid, worker.callback);
40
+ await service.router.consumeMessages(key, 'WORKER', service.guid, worker.callback);
41
41
  services.push(service);
42
42
  }
43
43
  }
@@ -78,12 +78,12 @@ class WorkerService {
78
78
  }
79
79
  await service.stream.init(service.namespace, service.appId, service.logger);
80
80
  }
81
- initStreamSignaler(worker, logger) {
82
- return new stream_1.StreamSignaler({
81
+ initRouter(worker, logger) {
82
+ return new router_1.Router({
83
83
  namespace: this.namespace,
84
84
  appId: this.appId,
85
85
  guid: this.guid,
86
- role: stream_2.StreamRole.WORKER,
86
+ role: stream_1.StreamRole.WORKER,
87
87
  topic: worker.topic,
88
88
  reclaimDelay: worker.reclaimDelay,
89
89
  reclaimCount: worker.reclaimCount,
@@ -96,10 +96,34 @@ class WorkerService {
96
96
  if (message.type === 'throttle') {
97
97
  self.throttle(message.throttle);
98
98
  }
99
+ else if (message.type === 'ping') {
100
+ self.sayPong(self.appId, self.guid, message.originator, message.details);
101
+ }
99
102
  };
100
103
  }
104
+ async sayPong(appId, guid, originator, details = false) {
105
+ let profile;
106
+ if (details) {
107
+ const params = {
108
+ appId: this.appId,
109
+ topic: this.topic,
110
+ };
111
+ profile = {
112
+ engine_id: this.guid,
113
+ namespace: this.namespace,
114
+ app_id: this.appId,
115
+ worker_topic: this.topic,
116
+ stream: this.stream.mintKey(key_1.KeyType.STREAMS, params),
117
+ };
118
+ }
119
+ this.store.publish(key_1.KeyType.QUORUM, {
120
+ type: 'pong',
121
+ guid, originator,
122
+ profile,
123
+ }, appId);
124
+ }
101
125
  async throttle(delayInMillis) {
102
- this.streamSignaler.setThrottle(delayInMillis);
126
+ this.router.setThrottle(delayInMillis);
103
127
  }
104
128
  }
105
129
  exports.WorkerService = WorkerService;
@@ -6,10 +6,12 @@ type ActivityData = {
6
6
  };
7
7
  type JobMetadata = {
8
8
  key?: string;
9
+ gid: string;
9
10
  jid: string;
10
11
  dad: string;
11
12
  aid: string;
12
13
  pj?: string;
14
+ pg?: string;
13
15
  pd?: string;
14
16
  pa?: string;
15
17
  ngn?: string;
@@ -1,7 +1,16 @@
1
1
  import { JobOutput } from "./job";
2
+ export interface QuorumProfile {
3
+ namespace: string;
4
+ app_id: string;
5
+ engine_id: string;
6
+ worker_topic?: string;
7
+ stream?: string;
8
+ stream_depth?: number;
9
+ }
2
10
  export interface PingMessage {
3
11
  type: 'ping';
4
12
  originator: string;
13
+ details?: boolean;
5
14
  }
6
15
  export interface WorkMessage {
7
16
  type: 'work';
@@ -13,8 +22,9 @@ export interface CronMessage {
13
22
  }
14
23
  export interface PongMessage {
15
24
  type: 'pong';
16
- originator: string;
17
25
  guid: string;
26
+ originator: string;
27
+ profile?: QuorumProfile;
18
28
  }
19
29
  export interface ActivateMessage {
20
30
  type: 'activate';
@@ -4,6 +4,7 @@ interface RedisMultiType {
4
4
  XADD(key: string, id: string, fields: any): this;
5
5
  XACK(key: string, group: string, id: string): this;
6
6
  XDEL(key: string, id: string): this;
7
+ XLEN(key: string): this;
7
8
  HDEL(key: string, itemId: string): this;
8
9
  HGET(key: string, itemId: string): this;
9
10
  HGETALL(key: string): this;
@@ -31,6 +31,7 @@ export interface StreamData {
31
31
  guid: string;
32
32
  topic?: string;
33
33
  jid?: string;
34
+ gid?: string;
34
35
  dad?: string;
35
36
  aid: string;
36
37
  trc?: string;
package/modules/enums.ts CHANGED
@@ -30,3 +30,6 @@ export const FIDELITY_SECONDS = process.env.NODE_ENV === 'test' ? TEST_FIDELITY_
30
30
 
31
31
  // DURABLE CONSTANTS
32
32
  export const DURABLE_EXPIRE_SECONDS = 1;
33
+
34
+ // TASK CONSTANTS
35
+ export const SCOUT_INTERVAL_SECONDS = 60;