@hotmeshio/hotmesh 0.0.34 → 0.0.36

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 (93) hide show
  1. package/README.md +30 -18
  2. package/build/modules/enums.d.ts +22 -0
  3. package/build/modules/enums.js +29 -0
  4. package/build/modules/errors.d.ts +10 -2
  5. package/build/modules/errors.js +14 -3
  6. package/build/modules/key.d.ts +16 -15
  7. package/build/modules/key.js +18 -15
  8. package/build/modules/utils.d.ts +1 -0
  9. package/build/modules/utils.js +6 -1
  10. package/build/package.json +3 -1
  11. package/build/services/activities/activity.d.ts +5 -0
  12. package/build/services/activities/activity.js +27 -6
  13. package/build/services/activities/await.js +11 -3
  14. package/build/services/activities/cycle.js +10 -2
  15. package/build/services/activities/hook.js +8 -2
  16. package/build/services/activities/index.d.ts +2 -2
  17. package/build/services/activities/index.js +2 -2
  18. package/build/services/activities/interrupt.d.ts +16 -0
  19. package/build/services/activities/interrupt.js +129 -0
  20. package/build/services/activities/signal.js +9 -2
  21. package/build/services/activities/trigger.d.ts +4 -0
  22. package/build/services/activities/trigger.js +14 -4
  23. package/build/services/activities/worker.js +10 -2
  24. package/build/services/collator/index.d.ts +4 -0
  25. package/build/services/collator/index.js +8 -0
  26. package/build/services/compiler/deployer.js +1 -3
  27. package/build/services/connector/index.js +2 -3
  28. package/build/services/durable/client.js +7 -3
  29. package/build/services/durable/factory.js +65 -284
  30. package/build/services/durable/handle.d.ts +37 -0
  31. package/build/services/durable/handle.js +52 -9
  32. package/build/services/durable/meshos.js +2 -2
  33. package/build/services/durable/worker.js +9 -2
  34. package/build/services/durable/workflow.d.ts +24 -0
  35. package/build/services/durable/workflow.js +56 -1
  36. package/build/services/engine/index.d.ts +14 -6
  37. package/build/services/engine/index.js +52 -27
  38. package/build/services/hotmesh/index.d.ts +3 -1
  39. package/build/services/hotmesh/index.js +11 -3
  40. package/build/services/quorum/index.d.ts +1 -0
  41. package/build/services/quorum/index.js +10 -0
  42. package/build/services/signaler/stream.js +25 -29
  43. package/build/services/store/clients/ioredis.js +1 -0
  44. package/build/services/store/index.d.ts +40 -4
  45. package/build/services/store/index.js +114 -9
  46. package/build/services/task/index.d.ts +5 -4
  47. package/build/services/task/index.js +12 -14
  48. package/build/types/activity.d.ts +35 -5
  49. package/build/types/durable.d.ts +4 -0
  50. package/build/types/index.d.ts +1 -1
  51. package/build/types/job.d.ts +18 -1
  52. package/build/types/quorum.d.ts +11 -7
  53. package/build/types/stream.d.ts +4 -1
  54. package/build/types/stream.js +2 -0
  55. package/modules/enums.ts +32 -0
  56. package/modules/errors.ts +24 -9
  57. package/modules/key.ts +4 -1
  58. package/modules/utils.ts +5 -0
  59. package/package.json +3 -1
  60. package/services/activities/activity.ts +34 -8
  61. package/services/activities/await.ts +11 -4
  62. package/services/activities/cycle.ts +10 -3
  63. package/services/activities/hook.ts +8 -3
  64. package/services/activities/index.ts +2 -2
  65. package/services/activities/interrupt.ts +159 -0
  66. package/services/activities/signal.ts +9 -3
  67. package/services/activities/trigger.ts +21 -5
  68. package/services/activities/worker.ts +10 -3
  69. package/services/collator/index.ts +10 -1
  70. package/services/compiler/deployer.ts +1 -3
  71. package/services/connector/index.ts +3 -5
  72. package/services/durable/client.ts +8 -4
  73. package/services/durable/factory.ts +65 -284
  74. package/services/durable/handle.ts +55 -9
  75. package/services/durable/meshos.ts +2 -3
  76. package/services/durable/worker.ts +9 -2
  77. package/services/durable/workflow.ts +66 -2
  78. package/services/engine/index.ts +74 -26
  79. package/services/hotmesh/index.ts +14 -4
  80. package/services/quorum/index.ts +9 -0
  81. package/services/signaler/stream.ts +27 -24
  82. package/services/store/clients/ioredis.ts +1 -0
  83. package/services/store/index.ts +119 -11
  84. package/services/task/index.ts +18 -18
  85. package/types/activity.ts +38 -8
  86. package/types/durable.ts +8 -4
  87. package/types/index.ts +1 -1
  88. package/types/job.ts +30 -1
  89. package/types/quorum.ts +13 -8
  90. package/types/stream.ts +3 -0
  91. package/build/services/activities/iterate.d.ts +0 -9
  92. package/build/services/activities/iterate.js +0 -13
  93. package/services/activities/iterate.ts +0 -26
@@ -1,5 +1,7 @@
1
- import { JobOutput } from '../../types/job';
1
+ import { STATUS_CODE_INTERRUPT } from '../../modules/enums';
2
2
  import { HotMeshService as HotMesh } from '../hotmesh';
3
+ import { JobInterruptOptions, JobOutput } from '../../types/job';
4
+ import { StreamError } from '../../types/stream';
3
5
 
4
6
  export class WorkflowHandleService {
5
7
  hotMesh: HotMesh;
@@ -12,10 +14,22 @@ export class WorkflowHandleService {
12
14
  this.hotMesh = hotMesh;
13
15
  }
14
16
 
17
+ /**
18
+ * Sends a signal to the workflow. This is a way to send
19
+ * a message to a workflow that is paused due to having
20
+ * executed a `waitForSignal` workflow extension. Awakens
21
+ * the workflow if no other signals are pending.
22
+ */
15
23
  async signal(signalId: string, data: Record<any, any>): Promise<void> {
16
24
  await this.hotMesh.hook(`${this.hotMesh.appId}.wfs.signal`, { id: signalId, data });
17
25
  }
18
26
 
27
+ /**
28
+ * Returns the job state of the workflow. If the workflow has completed
29
+ * this is also the job output. If the workflow is still running, this
30
+ * is the current state of the job, but it may change depending upon
31
+ * the activities that remain.
32
+ */
19
33
  async state(metadata = false): Promise<Record<string, any>> {
20
34
  const state = await this.hotMesh.getState(`${this.hotMesh.appId}.execute`, this.workflowId);
21
35
  if (!state.data && state.metadata.err) {
@@ -24,14 +38,41 @@ export class WorkflowHandleService {
24
38
  return metadata ? state : state.data;
25
39
  }
26
40
 
41
+ /**
42
+ * Returns the current search state of the workflow. This is
43
+ * different than the job state or individual activity state.
44
+ * Search state represents name/value pairs that were added
45
+ * to the workflow. As the workflow is stored in a Redis hash,
46
+ * this is a way to store additional data that is indexed
47
+ * and searchable using the RediSearch module.
48
+ */
27
49
  async queryState(fields: string[]): Promise<Record<string, any>> {
28
50
  return await this.hotMesh.getQueryState(this.workflowId, fields);
29
51
  }
30
52
 
53
+ /**
54
+ * Returns the current status of the workflow. This is a semaphore
55
+ * value that represents the current state of the workflow, where
56
+ * 0 is complete and a negative value represents that the flow was
57
+ * interrupted.
58
+ */
31
59
  async status(): Promise<number> {
32
60
  return await this.hotMesh.getStatus(this.workflowId);
33
61
  }
34
62
 
63
+ /**
64
+ * Interrupts a running workflow. Standard Job Completion tasks will
65
+ * run. Subscribers will be notified and the job hash will be expired.
66
+ */
67
+ async interrupt(options?: JobInterruptOptions): Promise<string> {
68
+ return await this.hotMesh.interrupt(`${this.hotMesh.appId}.execute`, this.workflowId, options);
69
+ }
70
+
71
+ /**
72
+ * Awaits for the workflow to complete and returns the result. If
73
+ * the workflow thows and error, this method will likewise throw
74
+ * an error.
75
+ */
35
76
  async result(loadState?: boolean): Promise<any> {
36
77
  if (loadState) {
37
78
  const state = await this.hotMesh.getState(`${this.hotMesh.appId}.execute`, this.workflowId);
@@ -58,29 +99,34 @@ export class WorkflowHandleService {
58
99
  return reject(JSON.parse(err));
59
100
  } else if (!response) {
60
101
  const state = await this.hotMesh.getState(`${this.hotMesh.appId}.execute`, this.workflowId);
61
- if (!state.data && state.metadata.err) {
62
- return reject(JSON.parse(state.metadata.err));
102
+ if (state.metadata.err) {
103
+ const error = JSON.parse(state.metadata.err) as StreamError;
104
+ if (error.code === STATUS_CODE_INTERRUPT || !state.data) {
105
+ return reject({ ...error, job_id: this.workflowId });
106
+ }
63
107
  }
64
108
  response = state.data?.response;
65
109
  }
66
110
  resolve(response);
67
111
  };
68
112
  //check for done
69
- if (status == 0) {
113
+ if (status <= 0) {
70
114
  return complete();
71
115
  }
72
116
  //subscribe to topic
73
117
  this.hotMesh.sub(topic, async (topic: string, state: JobOutput) => {
74
- if (!state.data && state.metadata.err) {
75
- await complete(null, state.metadata.err);
76
- } else {
77
- await complete(state.data?.response);
118
+ if (state.metadata.err) {
119
+ const error = JSON.parse(state.metadata.err) as StreamError;
120
+ if (error.code === STATUS_CODE_INTERRUPT || !state.data) {
121
+ return await complete(null, state.metadata.err);
122
+ }
78
123
  }
124
+ await complete(state.data?.response);
79
125
  });
80
126
  //resolve for race condition
81
127
  setTimeout(async () => {
82
128
  status = await this.hotMesh.getStatus(this.workflowId);
83
- if (status == 0) {
129
+ if (status <= 0) {
84
130
  await complete();
85
131
  }
86
132
  }, 0);
@@ -1,5 +1,3 @@
1
- import { nanoid } from 'nanoid';
2
-
3
1
  import { Durable } from '.';
4
2
  import { asyncLocalStorage } from './asyncLocalStorage';
5
3
  import { ClientService as Client } from './client';
@@ -18,6 +16,7 @@ import {
18
16
  WorkflowSearchOptions } from '../../types/durable';
19
17
  import { RedisOptions, RedisClass } from '../../types/redis';
20
18
  import { StringAnyType } from '../../types/serializer';
19
+ import { guid } from '../../modules/utils';
21
20
 
22
21
  /**
23
22
  * The base class for running MeshOS workflows.
@@ -117,7 +116,7 @@ export class MeshOSService {
117
116
  */
118
117
  static mintGuid(): string {
119
118
  const my = new this();
120
- return `${my.search?.prefix?.[0]}${nanoid()}`;
119
+ return `${my.search?.prefix?.[0]}${guid()}`;
121
120
  }
122
121
 
123
122
  /**
@@ -201,14 +201,21 @@ export class WorkerService {
201
201
  //incoming data payload has arguments and workflowId
202
202
  const workflowInput = data.data as unknown as WorkflowDataType;
203
203
  const context = new Map();
204
+ context.set('raw', data);
204
205
  context.set('namespace', config.namespace ?? APP_ID);
205
206
  context.set('counter', counter);
206
207
  context.set('workflowId', workflowInput.workflowId);
207
- if (data.data.workflowDimension) {
208
+ context.set('workflowId', workflowInput.workflowId);
209
+ if (workflowInput.originJobId) {
210
+ //if present there is an origin job to which this job is subordinated;
211
+ // garbage collect (expire) this job when originJobId is expired
212
+ context.set('originJobId', workflowInput.originJobId);
213
+ }
214
+ if (workflowInput.workflowDimension) {
208
215
  //every hook function runs in an isolated dimension controlled
209
216
  //by the index assigned when the signal was received; even if the
210
217
  //hook function re-runs, its scope will always remain constant
211
- context.set('workflowDimension', data.data.workflowDimension);
218
+ context.set('workflowDimension', workflowInput.workflowDimension);
212
219
  }
213
220
  context.set('workflowTopic', workflowTopic);
214
221
  context.set('workflowName', workflowTopic.split('-').pop());
@@ -19,7 +19,7 @@ import {
19
19
  ProxyType,
20
20
  WorkflowContext,
21
21
  WorkflowOptions } from "../../types/durable";
22
- import { JobOutput, JobState } from '../../types/job';
22
+ import { JobInterruptOptions, JobOutput, JobState } from '../../types/job';
23
23
  import { StreamStatus } from '../../types/stream';
24
24
  import { deterministicRandom } from '../../modules/utils';
25
25
 
@@ -35,6 +35,7 @@ export class WorkflowService {
35
35
  const store = asyncLocalStorage.getStore();
36
36
  const namespace = store.get('namespace');
37
37
  const workflowId = store.get('workflowId');
38
+ const originJobId = store.get('originJobId');
38
39
  const workflowDimension = store.get('workflowDimension') ?? '';
39
40
  const workflowTrace = store.get('workflowTrace');
40
41
  const workflowSpan = store.get('workflowSpan');
@@ -65,6 +66,7 @@ export class WorkflowService {
65
66
  ...options,
66
67
  namespace,
67
68
  workflowId: childJobId,
69
+ originJobId: originJobId ?? workflowId,
68
70
  parentWorkflowId,
69
71
  workflowTrace,
70
72
  workflowSpan,
@@ -289,6 +291,66 @@ export class WorkflowService {
289
291
  }
290
292
  }
291
293
 
294
+ static getLocalState() {
295
+ const store = asyncLocalStorage.getStore();
296
+ return {
297
+ workflowId: store.get('workflowId'),
298
+ namespace: store.get('namespace'),
299
+ workflowTopic: store.get('workflowTopic'),
300
+ workflowDimension: store.get('workflowDimension') ?? '',
301
+ counter: store.get('counter'),
302
+ }
303
+ }
304
+
305
+ /**
306
+ * Executes a function once and caches the result. If the function is called
307
+ * again, the cached result is returned. This is useful for wrapping
308
+ * expensive activity calls that should only be run once, but which might
309
+ * not require the configuration nuance/expense provided by proxyActivities.
310
+ * @template T - the result type
311
+ */
312
+ static async once<T>(fn: (...args: any[]) => Promise<T>, ...args: any[]): Promise<T> {
313
+ const {
314
+ workflowId,
315
+ namespace,
316
+ workflowTopic,
317
+ workflowDimension,
318
+ counter: COUNTER,
319
+ } = WorkflowService.getLocalState();
320
+ const execIndex = COUNTER.counter = COUNTER.counter + 1;
321
+ const sessionId = `-once${workflowDimension}-${execIndex}-`;
322
+ const hotMeshClient = await WorkerService.getHotMesh(workflowTopic, { namespace });
323
+ const keyParams = {
324
+ appId: hotMeshClient.appId,
325
+ jobId: workflowId
326
+ }
327
+ const workflowGuid = KeyService.mintKey(hotMeshClient.namespace, KeyType.JOB_STATE, keyParams);
328
+ const value = await hotMeshClient.engine.store.exec('HGET', workflowGuid, sessionId) as string;
329
+ if (value) {
330
+ return JSON.parse(value) as T;
331
+ }
332
+ const response = await fn(...args);
333
+ await hotMeshClient.engine.store.exec('HSET', workflowGuid, sessionId, JSON.stringify(response));
334
+ return response;
335
+ }
336
+
337
+ /**
338
+ * Interrupts a running job
339
+ *
340
+ * @param {string} jobId - the target job id
341
+ * @param {JobInterruptOptions} options - the interrupt options
342
+ * @returns {Promise<string>} - the stream id
343
+ */
344
+ static async interrupt(jobId: string, options: JobInterruptOptions = {}): Promise<string | void> {
345
+ const store = asyncLocalStorage.getStore();
346
+ const workflowTopic = store.get('workflowTopic');
347
+ const namespace = store.get('namespace');
348
+ const hotMeshClient = await WorkerService.getHotMesh(workflowTopic, { namespace });
349
+ if (await WorkflowService.isSideEffectAllowed(hotMeshClient, 'interrupt')) {
350
+ return await hotMeshClient.interrupt(`${hotMeshClient.appId}.execute`, jobId, options);
351
+ }
352
+ }
353
+
292
354
  /**
293
355
  * Sleeps the workflow for a duration. As the function is reentrant,
294
356
  * upon reentry, the function will traverse prior execution paths up
@@ -407,6 +469,7 @@ export class WorkflowService {
407
469
  //increment by state (not value) to avoid race conditions
408
470
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
409
471
  const workflowId = store.get('workflowId');
472
+ const originJobId = store.get('originJobId');
410
473
  const workflowDimension = store.get('workflowDimension') ?? '';
411
474
  const workflowTopic = store.get('workflowTopic');
412
475
  const trc = store.get('workflowTrace');
@@ -439,7 +502,8 @@ export class WorkflowService {
439
502
  const duration = ms(options?.startToCloseTimeout || '1 minute');
440
503
  const payload = {
441
504
  arguments: Array.from(arguments),
442
- //the parent id is provided to categorize this activity for later cleanup
505
+ //when the origin job is removed
506
+ originJobId: originJobId ?? workflowId,
443
507
  parentWorkflowId: `${workflowId}-a`,
444
508
  workflowId: activityJobId,
445
509
  workflowTopic: activityTopic,
@@ -1,7 +1,14 @@
1
1
  import { KeyType } from '../../modules/key';
2
+ import {
3
+ OTT_WAIT_TIME,
4
+ STATUS_CODE_SUCCESS,
5
+ STATUS_CODE_PENDING,
6
+ STATUS_CODE_TIMEOUT,
7
+ DURABLE_EXPIRE_SECONDS} from '../../modules/enums';
2
8
  import {
3
9
  formatISODate,
4
10
  getSubscriptionTopic,
11
+ guid,
5
12
  identifyRedisType,
6
13
  polyfill,
7
14
  restoreHierarchy } from '../../modules/utils';
@@ -9,6 +16,7 @@ import Activities from '../activities';
9
16
  import { Await } from '../activities/await';
10
17
  import { Cycle } from '../activities/cycle';
11
18
  import { Hook } from '../activities/hook';
19
+ import { Interrupt } from '../activities/interrupt';
12
20
  import { Signal } from '../activities/signal';
13
21
  import { Worker } from '../activities/worker';
14
22
  import { Trigger } from '../activities/trigger';
@@ -41,7 +49,9 @@ import {
41
49
  JobMetadata,
42
50
  JobOutput,
43
51
  PartialJobState,
44
- JobStatus } from '../../types/job';
52
+ JobStatus,
53
+ JobInterruptOptions,
54
+ JobCompletionOptions } from '../../types/job';
45
55
  import {
46
56
  HotMeshApps,
47
57
  HotMeshConfig,
@@ -69,12 +79,6 @@ import {
69
79
  StreamRole,
70
80
  StreamStatus } from '../../types/stream';
71
81
 
72
- //wait time to see if a job is complete
73
- const OTT_WAIT_TIME = 1000;
74
- const STATUS_CODE_SUCCESS = 200;
75
- const STATUS_CODE_PENDING = 202;
76
- const STATUS_CODE_TIMEOUT = 504;
77
-
78
82
  class EngineService {
79
83
  namespace: string;
80
84
  apps: HotMeshApps | null;
@@ -239,7 +243,7 @@ class EngineService {
239
243
  }
240
244
 
241
245
  // ************* METADATA/MODEL METHODS *************
242
- async initActivity(topic: string, data: JobData = {}, context?: JobState): Promise<Await|Cycle|Hook|Signal|Trigger|Worker> {
246
+ async initActivity(topic: string, data: JobData = {}, context?: JobState): Promise<Await|Cycle|Hook|Signal|Trigger|Worker|Interrupt> {
243
247
  const [activityId, schema] = await this.getSchema(topic);
244
248
  polyfill
245
249
  const ActivityHandler = Activities[polyfill.resolveActivityType(schema.type)];
@@ -329,6 +333,7 @@ class EngineService {
329
333
  aid: streamData.metadata.aid,
330
334
  status: streamData.status || StreamStatus.SUCCESS,
331
335
  code: streamData.code || 200,
336
+ type: streamData.type,
332
337
  });
333
338
  const context: PartialJobState = {
334
339
  metadata: {
@@ -380,6 +385,7 @@ class EngineService {
380
385
  const spn = context['$self']?.output?.metadata?.l2s || context['$self']?.output?.metadata?.l1s;
381
386
  const streamData: StreamData = {
382
387
  metadata: {
388
+ guid: guid(),
383
389
  jid: context.metadata.pj,
384
390
  dad: context.metadata.pd,
385
391
  aid: context.metadata.pa,
@@ -404,17 +410,28 @@ class EngineService {
404
410
  }
405
411
  }
406
412
  hasParentJob(context: JobState): boolean {
407
- //todo: include the dimensional address (pd)
408
413
  return Boolean(context.metadata.pj && context.metadata.pa);
409
414
  }
410
415
  resolveError(metadata: JobMetadata): StreamError | undefined {
411
416
  if (metadata && metadata.err) {
412
417
  return JSON.parse(metadata.err) as StreamError;
413
- }
418
+ }
419
+ }
420
+
421
+ // ****************** `INTERRUPT` ACTIVE JOBS *****************
422
+ async interrupt(topic: string, jobId: string, options: JobInterruptOptions = {}): Promise<string> {
423
+ await this.store.interrupt(topic, jobId, options);
424
+ const context = await this.getState(topic, jobId) as JobState;
425
+ const completionOpts: JobCompletionOptions = {
426
+ interrupt: options.descend,
427
+ expire: options.expire,
428
+ };
429
+ return await this.runJobCompletionTasks(context, completionOpts) as string;
414
430
  }
415
431
 
416
432
  // ****************** `SCRUB` CLEAN COMPLETED JOBS *****************
417
433
  async scrub(jobId: string) {
434
+ //todo: do not allow scrubbing of non-existent or actively running job
418
435
  await this.store.scrub(jobId);
419
436
  }
420
437
 
@@ -427,6 +444,7 @@ class EngineService {
427
444
  status,
428
445
  code,
429
446
  metadata: {
447
+ guid: guid(),
430
448
  aid,
431
449
  topic
432
450
  },
@@ -434,13 +452,23 @@ class EngineService {
434
452
  };
435
453
  return await this.streamSignaler.publishMessage(null, streamData) as string;
436
454
  }
437
- async hookTime(jobId: string, activityId: string): Promise<JobStatus | void> {
438
- //the activityid is concatenated with its dimensional address (dad); split to resolve
455
+ async hookTime(jobId: string, activityId: string, type?: 'sleep'|'expire'|'interrupt'): Promise<string | void> {
456
+ if (type === 'interrupt') {
457
+ return await this.interrupt(
458
+ activityId, //note: 'activityId' is the actually job topic
459
+ jobId,
460
+ { suppress: true, expire: 1 },
461
+ );
462
+ } else if (type === 'expire') {
463
+ return await this.store.expireJob(jobId, 1);
464
+ }
465
+ //'sleep': parse the activityId into parts
439
466
  const [aid, ...dimensions] = activityId.split(',');
440
467
  const dad = `,${dimensions.join(',')}`;
441
468
  const streamData: StreamData = {
442
469
  type: StreamDataType.TIMEHOOK,
443
470
  metadata: {
471
+ guid: guid(),
444
472
  jid: jobId,
445
473
  aid,
446
474
  dad,
@@ -540,7 +568,7 @@ class EngineService {
540
568
  }, timeout);
541
569
  });
542
570
  }
543
- async resolveOneTimeSubscription(context: JobState, jobOutput: JobOutput, emit = false) {
571
+ async pubOneTimeSubs(context: JobState, jobOutput: JobOutput, emit = false) {
544
572
  //todo: subscriber should query for the job...only publish minimum context needed
545
573
  if (this.hasOneTimeSubscription(context)) {
546
574
  const message: JobMessage = {
@@ -557,7 +585,7 @@ class EngineService {
557
585
  const schema = await this.store.getSchema(activityId, config);
558
586
  return schema.publishes;
559
587
  }
560
- async resolvePersistentSubscriptions(context: JobState, jobOutput: JobOutput, emit = false) {
588
+ async pubPermSubs(context: JobState, jobOutput: JobOutput, emit = false) {
561
589
  const topic = await this.getPublishesTopic(context);
562
590
  if (topic) {
563
591
  const message: JobMessage = {
@@ -584,22 +612,42 @@ class EngineService {
584
612
 
585
613
 
586
614
  // ********** JOB COMPLETION/CLEANUP (AND JOB EMIT) ***********
587
- async runJobCompletionTasks(context: JobState, emit = false) {
588
- //if 'emit' is true, the job isn't done. it's just emitting
615
+ async runJobCompletionTasks(context: JobState, options: JobCompletionOptions = {}): Promise<string | void> {
616
+ //'emit' indicates the job is still active
589
617
  const isAwait = this.hasParentJob(context);
590
- const isOneTimeSubscription = this.hasOneTimeSubscription(context);
618
+ const isOneTimeSub = this.hasOneTimeSubscription(context);
591
619
  const topic = await this.getPublishesTopic(context);
592
- if (isAwait || isOneTimeSubscription || topic) {
593
- const jobOutput = await this.getState(context.metadata.tpc, context.metadata.jid);
594
- //always wait for stream pub/sub
595
- await this.execAdjacentParent(context, jobOutput, emit);
596
- //no need to wait for standard pub/sub
597
- this.resolveOneTimeSubscription(context, jobOutput, emit);
598
- this.resolvePersistentSubscriptions(context, jobOutput, emit);
620
+ let msgId: string;
621
+ if (isAwait || isOneTimeSub || topic) {
622
+ const jobOutput = await this.getState(
623
+ context.metadata.tpc,
624
+ context.metadata.jid,
625
+ );
626
+ msgId = await this.execAdjacentParent(
627
+ context,
628
+ jobOutput,
629
+ options.emit,
630
+ );
631
+ this.pubOneTimeSubs(context, jobOutput, options.emit);
632
+ this.pubPermSubs(context, jobOutput, options.emit);
599
633
  }
600
- if (!emit) {
601
- this.task.registerJobForCleanup(context.metadata.jid, context.metadata.expire);
634
+ if (!options.emit) {
635
+ this.task.registerJobForCleanup(
636
+ context.metadata.jid,
637
+ this.resolveExpires(context, options),
638
+ options,
639
+ );
602
640
  }
641
+ return msgId;
642
+ }
643
+
644
+ /**
645
+ * Job hash expiration is typically reliant on the metadata field
646
+ * if the activity concludes normally. However, if the job is `interrupted`,
647
+ * it will be expired immediately.
648
+ */
649
+ resolveExpires(context: JobState, options: JobCompletionOptions): number {
650
+ return context.metadata.expire ?? options.expire ?? DURABLE_EXPIRE_SECONDS;
603
651
  }
604
652
 
605
653
 
@@ -1,5 +1,5 @@
1
- import { nanoid } from 'nanoid';
2
1
  import { HMNS } from '../../modules/key';
2
+ import { guid } from '../../modules/utils';
3
3
  import { RedisConnection } from '../connector/clients/redis';
4
4
  import { RedisConnection as IORedisConnection } from '../connector/clients/ioredis';
5
5
  import { EngineService } from '../engine';
@@ -11,7 +11,8 @@ import {
11
11
  JobState,
12
12
  JobData,
13
13
  JobOutput,
14
- JobStatus } from '../../types/job';
14
+ JobStatus,
15
+ JobInterruptOptions} from '../../types/job';
15
16
  import {
16
17
  HotMeshConfig,
17
18
  HotMeshManifest } from '../../types/hotmesh';
@@ -58,7 +59,7 @@ class HotMeshService {
58
59
 
59
60
  static async init(config: HotMeshConfig) {
60
61
  const instance = new HotMeshService();
61
- instance.guid = nanoid();
62
+ instance.guid = guid();
62
63
  instance.verifyAndSetNamespace(config.namespace);
63
64
  instance.verifyAndSetAppId(config.appId);
64
65
  instance.logger = new LoggerService(config.appId, instance.guid, config.name || '', config.logLevel);
@@ -69,7 +70,7 @@ class HotMeshService {
69
70
  }
70
71
 
71
72
  static guid(): string {
72
- return nanoid();
73
+ return guid();
73
74
  }
74
75
 
75
76
  async initEngine(config: HotMeshConfig, logger: ILogger): Promise<void> {
@@ -146,6 +147,10 @@ class HotMeshService {
146
147
  //activation is a quorum operation
147
148
  return await this.quorum?.activate(version, delay);
148
149
  }
150
+ async inventory(version: string, delay?: number): Promise<number> {
151
+ //get count of all peers
152
+ return await this.quorum?.inventory(delay);
153
+ }
149
154
 
150
155
  // ************* REPORTER METHODS *************
151
156
  async getStats(topic: string, query: JobStatsInput): Promise<StatsResponse> {
@@ -167,6 +172,11 @@ class HotMeshService {
167
172
  return await this.engine?.resolveQuery(topic, query);
168
173
  }
169
174
 
175
+ // ****************** `INTERRUPT` ACTIVE JOBS *****************
176
+ async interrupt(topic: string, jobId: string, options: JobInterruptOptions = {}): Promise<string> {
177
+ return await this.engine?.interrupt(topic, jobId, options);
178
+ }
179
+
170
180
  // ****************** `SCRUB` CLEAN COMPLETED JOBS *****************
171
181
  async scrub(jobId: string) {
172
182
  await this.engine?.scrub(jobId);
@@ -117,6 +117,8 @@ class QuorumService {
117
117
  self.engine.processWebHooks()
118
118
  } else if (message.type === 'job') {
119
119
  self.engine.routeToSubscribers(message.topic, message.job)
120
+ } else if (message.type === 'cron') {
121
+ self.engine.processTimeHooks();
120
122
  }
121
123
  //if there are any callbacks, call them
122
124
  if (self.callbacks.length > 0) {
@@ -164,6 +166,13 @@ class QuorumService {
164
166
 
165
167
 
166
168
  // ************* COMPILER METHODS *************
169
+ async inventory(delay = QUORUM_DELAY): Promise<number> {
170
+ await this.requestQuorum(delay);
171
+ const q1 = await this.requestQuorum(delay);
172
+ const q2 = await this.requestQuorum(delay);
173
+ const q3 = await this.requestQuorum(delay);
174
+ return Math.round((q1 + q2 + q3) / 3);
175
+ }
167
176
  async activate(version: string, delay = QUORUM_DELAY): Promise<boolean> {
168
177
  version = version.toString();
169
178
  const config = await this.engine.getVID();