@hotmeshio/hotmesh 0.0.38 → 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 (51) hide show
  1. package/README.md +13 -7
  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/router/index.js +16 -14
  20. package/build/services/store/index.d.ts +14 -9
  21. package/build/services/store/index.js +46 -23
  22. package/build/services/task/index.d.ts +10 -3
  23. package/build/services/task/index.js +35 -17
  24. package/build/types/durable.d.ts +3 -2
  25. package/build/types/hotmesh.d.ts +43 -2
  26. package/build/types/hotmesh.js +28 -0
  27. package/build/types/index.d.ts +3 -2
  28. package/build/types/index.js +3 -1
  29. package/build/types/logger.d.ts +1 -0
  30. package/build/types/logger.js +1 -0
  31. package/build/types/task.d.ts +1 -0
  32. package/build/types/task.js +2 -0
  33. package/modules/enums.ts +49 -35
  34. package/modules/errors.ts +17 -8
  35. package/modules/key.ts +3 -40
  36. package/package.json +1 -1
  37. package/services/activities/activity.ts +2 -2
  38. package/services/activities/hook.ts +18 -9
  39. package/services/activities/trigger.ts +10 -6
  40. package/services/durable/client.ts +31 -15
  41. package/services/durable/handle.ts +3 -3
  42. package/services/durable/worker.ts +4 -3
  43. package/services/engine/index.ts +13 -12
  44. package/services/router/index.ts +26 -24
  45. package/services/store/index.ts +59 -25
  46. package/services/task/index.ts +66 -24
  47. package/types/durable.ts +6 -5
  48. package/types/hotmesh.ts +47 -2
  49. package/types/index.ts +8 -1
  50. package/types/logger.ts +3 -1
  51. package/types/task.ts +1 -0
@@ -1,14 +1,14 @@
1
1
  import {
2
- BLOCK_TIME_MS,
3
- MAX_RETRIES,
4
- MAX_TIMEOUT_MS,
5
- GRADUATED_INTERVAL_MS,
6
- STATUS_CODE_UNACKED,
7
- STATUS_CODE_UNKNOWN,
8
- STATUS_MESSAGE_UNKNOWN,
9
- XCLAIM_COUNT,
10
- XCLAIM_DELAY_MS,
11
- XPENDING_COUNT } from '../../modules/enums';
2
+ HMSH_BLOCK_TIME_MS,
3
+ HMSH_MAX_RETRIES,
4
+ HMSH_MAX_TIMEOUT_MS,
5
+ HMSH_GRADUATED_INTERVAL_MS,
6
+ HMSH_CODE_UNACKED,
7
+ HMSH_CODE_UNKNOWN,
8
+ HMSH_STATUS_UNKNOWN,
9
+ HMSH_XCLAIM_COUNT,
10
+ HMSH_XCLAIM_DELAY_MS,
11
+ HMSH_XPENDING_COUNT } from '../../modules/enums';
12
12
  import { KeyType } from '../../modules/key';
13
13
  import { XSleepFor, guid, sleepFor } from '../../modules/utils';
14
14
  import { ILogger } from '../logger';
@@ -50,8 +50,8 @@ class Router {
50
50
  this.topic = config.topic;
51
51
  this.stream = stream;
52
52
  this.store = store;
53
- this.reclaimDelay = config.reclaimDelay || XCLAIM_DELAY_MS;
54
- this.reclaimCount = config.reclaimCount || XCLAIM_COUNT;
53
+ this.reclaimDelay = config.reclaimDelay || HMSH_XCLAIM_DELAY_MS;
54
+ this.reclaimCount = config.reclaimCount || HMSH_XCLAIM_COUNT;
55
55
  this.logger = logger;
56
56
  }
57
57
 
@@ -85,7 +85,9 @@ class Router {
85
85
  }
86
86
 
87
87
  try {
88
- const result = await this.stream.xreadgroup('GROUP', group, consumer, 'BLOCK', BLOCK_TIME_MS, 'STREAMS', stream, '>');
88
+ //randomizer that asymptotes at 150% of `HMSH_BLOCK_TIME_MS`
89
+ const streamDuration = HMSH_BLOCK_TIME_MS + Math.round((HMSH_BLOCK_TIME_MS * Math.random()));
90
+ const result = await this.stream.xreadgroup('GROUP', group, consumer, 'BLOCK', streamDuration, 'STREAMS', stream, '>');
89
91
  if (this.isStreamMessage(result)) {
90
92
  const [[, messages]] = result;
91
93
  for (const [id, message] of messages) {
@@ -107,7 +109,7 @@ class Router {
107
109
  if (this.shouldConsume && process.env.NODE_ENV !== 'test') {
108
110
  this.logger.error(`stream-consume-message-error`, { err, stream, group, consumer });
109
111
  this.errorCount++;
110
- const timeout = Math.min(GRADUATED_INTERVAL_MS * (2 ** this.errorCount), MAX_TIMEOUT_MS);
112
+ const timeout = Math.min(HMSH_GRADUATED_INTERVAL_MS * (2 ** this.errorCount), HMSH_MAX_TIMEOUT_MS);
111
113
  setTimeout(consume.bind(this), timeout);
112
114
  }
113
115
  }
@@ -129,7 +131,7 @@ class Router {
129
131
  telemetry.startStreamSpan(input, this.role);
130
132
  output = await this.execStreamLeg(input, stream, id, callback.bind(this));
131
133
  if (output?.status === StreamStatus.ERROR) {
132
- telemetry.setStreamError(`Function Status Code ${ output.code || STATUS_CODE_UNKNOWN }`);
134
+ telemetry.setStreamError(`Function Status Code ${ output.code || HMSH_CODE_UNKNOWN }`);
133
135
  }
134
136
  this.errorCount = 0;
135
137
  } catch (err) {
@@ -191,7 +193,7 @@ class Router {
191
193
  const errorCode = output.code.toString();
192
194
  const policy = policies?.[errorCode];
193
195
  const maxRetries = policy?.[0];
194
- const tryCount = Math.min(input.metadata.try || 0, MAX_RETRIES);
196
+ const tryCount = Math.min(input.metadata.try || 0, HMSH_MAX_RETRIES);
195
197
  //only possible values for maxRetries are 1, 2, 3
196
198
  //only possible values for tryCount are 0, 1, 2
197
199
  if (maxRetries > tryCount) {
@@ -206,7 +208,7 @@ class Router {
206
208
  if (typeof err.message === 'string') {
207
209
  error.message = err.message;
208
210
  } else {
209
- error.message = STATUS_MESSAGE_UNKNOWN;
211
+ error.message = HMSH_STATUS_UNKNOWN;
210
212
  }
211
213
  if (typeof err.stack === 'string') {
212
214
  error.stack = err.stack;
@@ -216,7 +218,7 @@ class Router {
216
218
  }
217
219
  return {
218
220
  status: 'error',
219
- code: STATUS_CODE_UNKNOWN,
221
+ code: HMSH_CODE_UNKNOWN,
220
222
  metadata: { ...input.metadata, guid: guid() },
221
223
  data: error as StreamError
222
224
  } as StreamDataResponse;
@@ -224,7 +226,7 @@ class Router {
224
226
 
225
227
  structureUnacknowledgedError(input: StreamData) {
226
228
  const message = 'stream message max delivery count exceeded';
227
- const code = STATUS_CODE_UNACKED;
229
+ const code = HMSH_CODE_UNACKED;
228
230
  const data: StreamError = { message, code };
229
231
  const output: StreamDataResponse = {
230
232
  metadata: { ...input.metadata, guid: guid() },
@@ -238,9 +240,9 @@ class Router {
238
240
  }
239
241
 
240
242
  structureError(input: StreamData, output: StreamDataResponse): StreamDataResponse {
241
- const message = output.data?.message ? output.data?.message.toString() : STATUS_MESSAGE_UNKNOWN;
243
+ const message = output.data?.message ? output.data?.message.toString() : HMSH_STATUS_UNKNOWN;
242
244
  const statusCode = output.code || output.data?.code;
243
- const code = isNaN(statusCode as number) ? STATUS_CODE_UNKNOWN : parseInt(statusCode.toString());
245
+ const code = isNaN(statusCode as number) ? HMSH_CODE_UNKNOWN : parseInt(statusCode.toString());
244
246
  const data: StreamError = { message, code };
245
247
  if (typeof output.data?.error === 'object') {
246
248
  data.error = { ...output.data.error };
@@ -257,7 +259,7 @@ class Router {
257
259
  for (const instance of [...Router.instances]) {
258
260
  instance.stopConsuming();
259
261
  }
260
- await sleepFor(BLOCK_TIME_MS);
262
+ await sleepFor(HMSH_BLOCK_TIME_MS * 2);
261
263
  }
262
264
 
263
265
  async stopConsuming() {
@@ -281,7 +283,7 @@ class Router {
281
283
  this.logger.info(`stream-throttle-reset`, { delay: this.throttle, topic: this.topic });
282
284
  }
283
285
 
284
- async claimUnacknowledged(stream: string, group: string, consumer: string, idleTimeMs = this.reclaimDelay, limit = XPENDING_COUNT): Promise<[string, [string, string]][]> {
286
+ async claimUnacknowledged(stream: string, group: string, consumer: string, idleTimeMs = this.reclaimDelay, limit = HMSH_XPENDING_COUNT): Promise<[string, [string, string]][]> {
285
287
  let pendingMessages = [];
286
288
  const pendingMessagesInfo = await this.stream.xpending(stream, group, '-', '+', limit); //[[ '1688768134881-0', 'testConsumer1', 1017, 1 ]]
287
289
  for (const pendingMessageInfo of pendingMessagesInfo) {
@@ -310,7 +312,7 @@ class Router {
310
312
  // ii) corrupt hardware/network/transport/etc
311
313
  // 3b) system error: Redis unable to accept `xadd` request
312
314
  // 4c) system error: Redis unable to accept `xdel`/`xack` request
313
- this.logger.error('stream-message-max-delivery-count-exceeded', { id, stream, group, consumer, code: STATUS_CODE_UNACKED, count });
315
+ this.logger.error('stream-message-max-delivery-count-exceeded', { id, stream, group, consumer, code: HMSH_CODE_UNACKED, count });
314
316
  const streamData = reclaimedMessage[0]?.[1]?.[1];
315
317
 
316
318
  //fatal risk point 1 of 3): json is corrupt
@@ -29,8 +29,9 @@ 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 { SCOUT_INTERVAL_SECONDS, STATUS_CODE_INTERRUPT } from '../../modules/enums';
32
+ import { HMSH_SCOUT_INTERVAL_SECONDS, HMSH_CODE_INTERRUPT } from '../../modules/enums';
33
33
  import { GetStateError } from '../../modules/errors';
34
+ import { WorkListTaskType } from '../../types/task';
34
35
 
35
36
  interface AbstractRedisClient {
36
37
  exec(): any;
@@ -177,7 +178,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
177
178
  * check for and process work items in the
178
179
  * time and signal task queues.
179
180
  */
180
- async reserveScoutRole(scoutType: 'time' | 'signal', delay = SCOUT_INTERVAL_SECONDS): Promise<boolean> {
181
+ async reserveScoutRole(scoutType: 'time' | 'signal', delay = HMSH_SCOUT_INTERVAL_SECONDS): Promise<boolean> {
181
182
  const key = this.mintKey(KeyType.WORK_ITEMS, { appId: this.appId, scoutType });
182
183
  const success = await this.redisClient[this.commands.setnx](key, `${scoutType}:${formatISODate(new Date())}`);
183
184
  if (this.isSuccessful(success)) {
@@ -388,16 +389,45 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
388
389
  }
389
390
 
390
391
  /**
391
- * Registers jobId with the originJobId that spawned it. In the future,
392
- * when originJobId is interrupted or expired, the items in the
393
- * list (added via RPUSH) are LPOPed. If origin was expired, then
394
- * LPOPed items from the list are likewise expired;
392
+ * Registers the job, `jobId`, with `originJobId`. In the future,
393
+ * when `originJobId` is interrupted/expired, the items in the
394
+ * list (added via RPUSH) will be interrupted/expired (removed via LPOPed).
395
395
  */
396
- async setDependency(originJobId: string, topic: string, jobId: string, gId: string, multi? : U): Promise<any> {
396
+ async registerJobDependency(originJobId: string, topic: string, jobId: string, gId: string, multi? : U): Promise<any> {
397
397
  const privateMulti = multi || this.getMulti();
398
- const depParams = { appId: this.appId, jobId: originJobId };
399
- const depKey = this.mintKey(KeyType.JOB_DEPENDENTS, depParams);
400
- privateMulti[this.commands.rpush](depKey, `expire::${topic}::${gId}::${jobId}`);
398
+ const dependencyParams = {
399
+ appId: this.appId,
400
+ jobId: originJobId,
401
+ };
402
+ const depKey = this.mintKey(
403
+ KeyType.JOB_DEPENDENTS,
404
+ dependencyParams,
405
+ );
406
+ //tasks have '4' segments
407
+ const expireTask = `expire::${topic}::${gId}::${jobId}`;
408
+ privateMulti[this.commands.rpush](depKey, expireTask);
409
+ if (!multi) {
410
+ return await privateMulti.exec();
411
+ }
412
+ }
413
+
414
+ /**
415
+ * Ensures a `hook signal` is delisted when its parent activity/job
416
+ * is interrupted/expired.
417
+ */
418
+ async registerSignalDependency(jobId: string, signalKey: string, multi? : U): Promise<any> {
419
+ const privateMulti = multi || this.getMulti();
420
+ const dependencyParams = { appId: this.appId, jobId };
421
+ const dependencyKey = this.mintKey(
422
+ KeyType.JOB_DEPENDENTS,
423
+ dependencyParams,
424
+ );
425
+ //tasks have '4' segments
426
+ const delistTask = `delist::signal::${jobId}::${signalKey}`;
427
+ privateMulti[this.commands.rpush](
428
+ dependencyKey,
429
+ delistTask,
430
+ );
401
431
  if (!multi) {
402
432
  return await privateMulti.exec();
403
433
  }
@@ -710,11 +740,11 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
710
740
 
711
741
  async setHookSignal(hook: HookSignal, multi?: U): Promise<any> {
712
742
  const key = this.mintKey(KeyType.SIGNALS, { appId: this.appId });
713
- const { topic, resolved, jobId} = hook;
714
- const payload = {
715
- [`${topic}:${resolved}`]: jobId
716
- };
717
- return await (multi || this.redisClient)[this.commands.hset](key, payload);
743
+ const { topic, resolved, jobId} = hook; //`${activityId}::${dad}::${gId}::${jobId}`
744
+ const signalKey = `${topic}:${resolved}`;
745
+ const payload = { [signalKey]: jobId };
746
+ await (multi || this.redisClient)[this.commands.hset](key, payload);
747
+ return await this.registerSignalDependency(jobId.split('::')[3], signalKey, multi);
718
748
  }
719
749
 
720
750
  async getHookSignal(topic: string, resolved: string): Promise<string | undefined> {
@@ -782,7 +812,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
782
812
  * expired at a future date; options indicate whether this
783
813
  * is a standard `expire` or an `interrupt`
784
814
  */
785
- async registerExpireJob(jobId: string, deletionTime: number, options: JobCompletionOptions): Promise<void> {
815
+ async registerDependenciesForCleanup(jobId: string, deletionTime: number, options: JobCompletionOptions): Promise<void> {
786
816
  const depParams = { appId: this.appId, jobId };
787
817
  const depKey = this.mintKey(KeyType.JOB_DEPENDENTS, depParams);
788
818
  const context = options.interrupt ? 'INTERRUPT' : 'EXPIRE';
@@ -797,7 +827,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
797
827
  * for the given sleep group. Sleep groups are
798
828
  * organized into 'n'-second blocks (LISTS))
799
829
  */
800
- async registerTimeHook(jobId: string, gId: string, activityId: string, type: 'sleep'|'expire'|'interrupt', deletionTime: number, multi?: U): Promise<void> {
830
+ async registerTimeHook(jobId: string, gId: string, activityId: string, type: WorkListTaskType, deletionTime: number, multi?: U): Promise<void> {
801
831
  const listKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId, timeValue: deletionTime });
802
832
  const timeEvent = `${type}::${activityId}::${gId}::${jobId}`;
803
833
  const len = await (multi || this.redisClient)[this.commands.rpush](listKey, timeEvent);
@@ -807,21 +837,25 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
807
837
  }
808
838
  }
809
839
 
810
- async getNextTimeJob(listKey?: string): Promise<[listKey: string, jobId: string, gId: string, activityId: string, type: 'sleep'|'expire'|'interrupt'] | boolean> {
811
- const existing = Boolean(listKey);
840
+ async getNextTask(listKey?: string): Promise<[listKey: string, jobId: string, gId: string, activityId: string, type: WorkListTaskType] | boolean> {
812
841
  const zsetKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId });
813
842
  listKey = listKey || await this.zRangeByScore(zsetKey, 0, Date.now());
814
843
  if (listKey) {
815
- const [pType, pKey] = this.resolveKeyContext(listKey);
844
+ let [pType, pKey] = this.resolveTaskKeyContext(listKey);
816
845
  const timeEvent = await this.redisClient[this.commands.lpop](pKey);
817
846
  if (timeEvent) {
818
- //there are 3 time-related event triggers: sleep, expire, interrupt
819
- const [_type, activityId, gId, ...jobId] = timeEvent.split('::');
847
+ //there are 4 time-related task
848
+ //1) sleep (awaken), 2) expire, 3) interrupt, 4) delist
849
+ const [type, activityId, gId, ...jobId] = timeEvent.split('::');
850
+ if (type === 'delist') {
851
+ pType = 'delist';
852
+ }
820
853
  return [listKey, jobId.join('::'), gId, activityId, pType];
821
854
  }
822
855
  await this.redisClient[this.commands.zrem](zsetKey, listKey);
856
+ return true;
823
857
  }
824
- return existing;
858
+ return false;
825
859
  }
826
860
 
827
861
  /**
@@ -832,7 +866,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
832
866
  * generic LIST (lists typically contain target job ids)
833
867
  * @param {string} listKey - for example `::INTERRUPT::job123` or `job123`
834
868
  */
835
- resolveKeyContext(listKey: string): [('sleep'|'expire'|'interrupt'), string] {
869
+ resolveTaskKeyContext(listKey: string): [('sleep'|'expire'|'interrupt'|'delist'), string] {
836
870
  if (listKey.startsWith('::INTERRUPT')) {
837
871
  return ['interrupt', listKey.split('::')[2]];
838
872
  } else if (listKey.startsWith('::EXPIRE')) {
@@ -874,7 +908,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
874
908
 
875
909
  //persists the standard 410 error (job is `gone`)
876
910
  const err = JSON.stringify({
877
- code: STATUS_CODE_INTERRUPT,
911
+ code: HMSH_CODE_INTERRUPT,
878
912
  message: options.reason ?? `job [${jobId}] interrupted`,
879
913
  job_id: jobId
880
914
  });
@@ -1,14 +1,16 @@
1
1
  import {
2
- EXPIRE_DURATION,
3
- FIDELITY_SECONDS,
4
- SCOUT_INTERVAL_SECONDS} from '../../modules/enums';
2
+ HMSH_EXPIRE_DURATION,
3
+ HMSH_FIDELITY_SECONDS,
4
+ HMSH_SCOUT_INTERVAL_SECONDS} from '../../modules/enums';
5
5
  import { XSleepFor, sleepFor } from '../../modules/utils';
6
6
  import { ILogger } from '../logger';
7
+ import { Pipe } from '../pipe';
7
8
  import { StoreService } from '../store';
8
9
  import { HookInterface, HookRule, HookSignal } from '../../types/hook';
10
+ import { KeyType } from '../../types/hotmesh';
9
11
  import { JobCompletionOptions, JobState } from '../../types/job';
10
12
  import { RedisClient, RedisMulti } from '../../types/redis';
11
- import { Pipe } from '../pipe';
13
+ import { WorkListTaskType } from '../../types/task';
12
14
 
13
15
  class TaskService {
14
16
  store: StoreService<RedisClient, RedisMulti>;
@@ -35,7 +37,12 @@ class TaskService {
35
37
  //todo: don't use 'id', make configurable using hook rule
36
38
  await hookEventCallback(topic, { ...data, id: jobId });
37
39
  } else {
38
- await this.store.deleteProcessedTaskQueue(workItemKey, sourceKey, destinationKey, scrub === 'true');
40
+ await this.store.deleteProcessedTaskQueue(
41
+ workItemKey,
42
+ sourceKey,
43
+ destinationKey,
44
+ scrub === 'true'
45
+ );
39
46
  }
40
47
  setImmediate(() => this.processWebHooks(hookEventCallback));
41
48
  }
@@ -45,21 +52,45 @@ class TaskService {
45
52
  await this.store.addTaskQueues(keys);
46
53
  }
47
54
 
48
- async registerJobForCleanup(jobId: string, inSeconds = EXPIRE_DURATION, options: JobCompletionOptions): Promise<void> {
55
+ async registerJobForCleanup(jobId: string, inSeconds = HMSH_EXPIRE_DURATION, options: JobCompletionOptions): Promise<void> {
49
56
  if (inSeconds > 0) {
50
57
  await this.store.expireJob(jobId, inSeconds);
51
- const expireTimeSlot = Math.floor((Date.now() + (inSeconds * 1000)) / (FIDELITY_SECONDS * 1000)) * (FIDELITY_SECONDS * 1000); //n second awaken groups
52
- await this.store.registerExpireJob(jobId, expireTimeSlot, options);
58
+ const fromNow = Date.now() + (inSeconds * 1000);
59
+ const fidelityMS = HMSH_FIDELITY_SECONDS * 1000;
60
+ const timeSlot = Math.floor(fromNow / fidelityMS) * fidelityMS;
61
+ await this.store.registerDependenciesForCleanup(
62
+ jobId,
63
+ timeSlot,
64
+ options,
65
+ );
53
66
  }
54
67
  }
55
68
 
56
- async registerTimeHook(jobId: string, gId: string, activityId: string, type: 'sleep'|'expire'|'interrupt', inSeconds = FIDELITY_SECONDS, multi?: RedisMulti): Promise<void> {
57
- const awakenTimeSlot = Math.floor((Date.now() + (inSeconds * 1000)) / (FIDELITY_SECONDS * 1000)) * (FIDELITY_SECONDS * 1000); //n second awaken groups
58
- await this.store.registerTimeHook(jobId, gId, activityId, type, awakenTimeSlot, multi);
69
+ async registerTimeHook(
70
+ jobId: string,
71
+ gId: string,
72
+ activityId: string,
73
+ type: WorkListTaskType,
74
+ inSeconds = HMSH_FIDELITY_SECONDS,
75
+ multi?: RedisMulti,
76
+ ): Promise<void> {
77
+ const fromNow = Date.now() + (inSeconds * 1000);
78
+ const fidelityMS = HMSH_FIDELITY_SECONDS * 1000;
79
+ const awakenTimeSlot = Math.floor(fromNow / fidelityMS) * fidelityMS;
80
+ await this.store.registerTimeHook(
81
+ jobId,
82
+ gId,
83
+ activityId,
84
+ type,
85
+ awakenTimeSlot,
86
+ multi,
87
+ );
59
88
  }
60
89
 
61
90
  /**
62
- * Should this engine instance play the role of 'scout' for the quorum.
91
+ * Should this engine instance play the role of 'scout' on behalf
92
+ * of the entire quorum? The scout role is responsible for processing
93
+ * task lists on behalf of the collective.
63
94
  */
64
95
  async shouldScout() {
65
96
  const wasScout = this.isScout;
@@ -68,30 +99,41 @@ class TaskService {
68
99
  if (!wasScout) {
69
100
  setTimeout(() => {
70
101
  this.isScout = false;
71
- }, SCOUT_INTERVAL_SECONDS * 1_000);
102
+ }, HMSH_SCOUT_INTERVAL_SECONDS * 1_000);
72
103
  }
73
104
  return true;
74
105
  }
75
106
  return false;
76
107
  }
77
108
 
78
- async processTimeHooks(timeEventCallback: (jobId: string, gId: string, activityId: string, type: 'sleep'|'expire'|'interrupt') => Promise<void>, listKey?: string): Promise<void> {
109
+ /**
110
+ * Callback handler that takes an item from a work list and
111
+ * processes according to its type
112
+ */
113
+ async processTimeHooks(timeEventCallback: (jobId: string, gId: string, activityId: string, type: WorkListTaskType) => Promise<void>, listKey?: string): Promise<void> {
79
114
  if (await this.shouldScout()) {
80
115
  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);
116
+ const workListTask = await this.store.getNextTask(listKey);
117
+
118
+ if (Array.isArray(workListTask)) {
119
+ const [listKey, target, gId, activityId, type] = workListTask;
120
+ if (type === 'delist') {
121
+ //delist the signalKey (target)
122
+ const key = this.store.mintKey(KeyType.SIGNALS, { appId: this.store.appId });
123
+ await this.store.redisClient[this.store.commands.hdel](key, target);
124
+ } else {
125
+ //awaken/expire/interrupt
126
+ await timeEventCallback(target, gId, activityId, type);
127
+ }
86
128
  await sleepFor(0);
87
129
  this.processTimeHooks(timeEventCallback, listKey);
88
- } else if (timeJob) {
89
- //a queue was just emptied; try again immediately
130
+ } else if (workListTask) {
131
+ //a worklist was just emptied; try again immediately
90
132
  await sleepFor(0);
91
133
  this.processTimeHooks(timeEventCallback);
92
134
  } else {
93
- //all queues are empty; sleep before checking
94
- let sleep = XSleepFor(FIDELITY_SECONDS * 1000);
135
+ //no worklists exist; sleep before checking
136
+ let sleep = XSleepFor(HMSH_FIDELITY_SECONDS * 1000);
95
137
  this.cleanupTimeout = sleep.timerId;
96
138
  await sleep.promise;
97
139
  this.processTimeHooks(timeEventCallback);
@@ -102,7 +144,7 @@ class TaskService {
102
144
  }
103
145
  } else {
104
146
  //didn't get the scout role; try again in 'one-ish' minutes
105
- let sleep = XSleepFor(SCOUT_INTERVAL_SECONDS * 1_000 * 2 * Math.random());
147
+ let sleep = XSleepFor(HMSH_SCOUT_INTERVAL_SECONDS * 1_000 * 2 * Math.random());
106
148
  this.cleanupTimeout = sleep.timerId;
107
149
  await sleep.promise;
108
150
  this.processTimeHooks(timeEventCallback);
package/types/durable.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { LogLevel } from './logger';
1
2
  import { RedisClass, RedisOptions } from './redis';
2
3
 
3
4
  type WorkflowConfig = {
@@ -173,16 +174,16 @@ type MeshOSActivityOptions = {
173
174
  }
174
175
 
175
176
  type MeshOSWorkerOptions = {
176
- taskQueue?: string; //change the default task queue
177
+ taskQueue?: string; //change the default task queue
177
178
  allowList?: Array<MeshOSOptions | string>; //limit which `hook` and `workflow` workers start
178
- logLevel?: string; //debug, info, warn, error
179
- maxSystemRetries?: number; //1-3 (10ms, 100ms, 1_000ms)
179
+ logLevel?: LogLevel; //debug, info, warn, error
180
+ maxSystemRetries?: number; //1-3 (10ms, 100ms, 1_000ms)
180
181
  backoffCoefficient?: number; //2-10ish
181
182
  }
182
183
 
183
184
  type WorkerOptions = {
184
- logLevel?: string; //debug, info, warn, error
185
- maxSystemRetries?: number; //1-3 (10ms, 100ms, 1_000ms)
185
+ logLevel?: LogLevel; //debug, info, warn, error
186
+ maxSystemRetries?: number; //1-3 (10ms, 100ms, 1_000ms)
186
187
  backoffCoefficient?: number; //2-10ish
187
188
  }
188
189
 
package/types/hotmesh.ts CHANGED
@@ -3,6 +3,49 @@ import { HotMeshService } from '../services/hotmesh';
3
3
  import { HookRules } from './hook';
4
4
  import { RedisClass, RedisClient, RedisOptions } from './redis';
5
5
  import { StreamData, StreamDataResponse } from './stream';
6
+ import { LogLevel } from './logger';
7
+
8
+ /**
9
+ * the full set of entity types that are stored in the key/value store
10
+ */
11
+ enum KeyType {
12
+ APP = 'APP',
13
+ ENGINE_ID = 'ENGINE',
14
+ HOOKS = 'HOOKS',
15
+ JOB_DEPENDENTS = 'JOB_DEPENDENTS',
16
+ JOB_STATE = 'JOB_STATE',
17
+ JOB_STATS_GENERAL = 'JOB_STATS_GENERAL',
18
+ JOB_STATS_MEDIAN = 'JOB_STATS_MEDIAN',
19
+ JOB_STATS_INDEX = 'JOB_STATS_INDEX',
20
+ HOTMESH = 'HOTMESH',
21
+ QUORUM = 'QUORUM',
22
+ SCHEMAS = 'SCHEMAS',
23
+ SIGNALS = 'SIGNALS',
24
+ STREAMS = 'STREAMS',
25
+ SUBSCRIPTIONS = 'SUBSCRIPTIONS',
26
+ SUBSCRIPTION_PATTERNS = 'SUBSCRIPTION_PATTERNS',
27
+ SYMKEYS = 'SYMKEYS',
28
+ SYMVALS = 'SYMVALS',
29
+ TIME_RANGE = 'TIME_RANGE',
30
+ WORK_ITEMS = 'WORK_ITEMS',
31
+ };
32
+
33
+ /**
34
+ * minting keys, requires one or more of the following parameters
35
+ */
36
+ type KeyStoreParams = {
37
+ appId?: string; //app id is a uuid for a hotmesh app
38
+ engineId?: string; //unique auto-generated guid for an ephemeral engine instance
39
+ appVersion?: string; //(e.g. "1.0.0", "1", "1.0")
40
+ jobId?: string; //a customer-defined id for job; must be unique for the entire app
41
+ activityId?: string; //activity id is a uuid for a given hotmesh app
42
+ jobKey?: string; //a customer-defined label for a job that serves to categorize events
43
+ dateTime?: string; //UTC date time: YYYY-MM-DDTHH:MM (20203-04-12T00:00); serves as a time-series bucket for the job_key
44
+ facet?: string; //data path starting at root with values separated by colons (e.g. "object/type:bar")
45
+ topic?: string; //topic name (e.g., "foo" or "" for top-level)
46
+ timeValue?: number; //time value (rounded to minute) (for delete range)
47
+ scoutType?: 'signal' | 'time'; //a single member of the quorum serves as the 'scout' for the group, triaging tasks for the collective
48
+ };
6
49
 
7
50
  type HotMesh = typeof HotMeshService;
8
51
 
@@ -39,7 +82,7 @@ type HotMeshConfig = {
39
82
  namespace?: string;
40
83
  name?: string;
41
84
  logger?: ILogger;
42
- logLevel?: 'silly' | 'debug' | 'info' | 'warn' | 'error' | 'silent';
85
+ logLevel?: LogLevel;
43
86
  engine?: HotMeshEngine;
44
87
  workers?: HotMeshWorker[];
45
88
  }
@@ -98,5 +141,7 @@ export {
98
141
  HotMeshApps, //object array of all apps in the db
99
142
  HotMeshConfig, //customer config
100
143
  HotMeshManifest,
101
- HotMeshGraph
144
+ HotMeshGraph,
145
+ KeyType,
146
+ KeyStoreParams,
102
147
  };
package/types/index.ts CHANGED
@@ -90,7 +90,9 @@ export {
90
90
  HotMeshGraph,
91
91
  HotMeshManifest,
92
92
  HotMeshSettings,
93
- HotMeshWorker } from './hotmesh';
93
+ HotMeshWorker,
94
+ KeyStoreParams,
95
+ KeyType } from './hotmesh';
94
96
  export {
95
97
  ActivateMessage,
96
98
  JobMessage,
@@ -98,6 +100,8 @@ export {
98
100
  PingMessage,
99
101
  PongMessage,
100
102
  QuorumMessage,
103
+ QuorumMessageCallback,
104
+ QuorumProfile,
101
105
  SubscriptionCallback,
102
106
  ThrottleMessage,
103
107
  WorkMessage } from './quorum';
@@ -161,7 +165,10 @@ export {
161
165
  trace,
162
166
  Tracer,
163
167
  ValueType } from './telemetry';
168
+ export {
169
+ WorkListTaskType } from './task'
164
170
  export {
165
171
  TransitionMatch,
166
172
  TransitionRule,
167
173
  Transitions } from './transition';
174
+
package/types/logger.ts CHANGED
@@ -3,4 +3,6 @@ export interface ILogger {
3
3
  error(message: string, ...meta: any[]): void;
4
4
  warn(message: string, ...meta: any[]): void;
5
5
  debug(message: string, ...meta: any[]): void;
6
- }
6
+ };
7
+
8
+ export type LogLevel = 'silly' | 'debug' | 'info' | 'warn' | 'error' | 'silent';
package/types/task.ts ADDED
@@ -0,0 +1 @@
1
+ export type WorkListTaskType = 'sleep' | 'expire' | 'interrupt' | 'delist';