@hotmeshio/hotmesh 0.0.33 → 0.0.35

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 (94) 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 +4 -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 +9 -6
  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/index.d.ts +5 -0
  33. package/build/services/durable/index.js +10 -0
  34. package/build/services/durable/meshos.js +3 -6
  35. package/build/services/durable/worker.js +11 -5
  36. package/build/services/durable/workflow.d.ts +24 -0
  37. package/build/services/durable/workflow.js +56 -1
  38. package/build/services/engine/index.d.ts +14 -6
  39. package/build/services/engine/index.js +52 -27
  40. package/build/services/hotmesh/index.d.ts +6 -2
  41. package/build/services/hotmesh/index.js +23 -5
  42. package/build/services/quorum/index.d.ts +1 -0
  43. package/build/services/quorum/index.js +10 -0
  44. package/build/services/signaler/stream.js +25 -29
  45. package/build/services/store/index.d.ts +40 -4
  46. package/build/services/store/index.js +114 -9
  47. package/build/services/task/index.d.ts +5 -4
  48. package/build/services/task/index.js +12 -14
  49. package/build/types/activity.d.ts +35 -5
  50. package/build/types/durable.d.ts +4 -0
  51. package/build/types/index.d.ts +1 -1
  52. package/build/types/job.d.ts +18 -1
  53. package/build/types/quorum.d.ts +11 -7
  54. package/build/types/stream.d.ts +4 -1
  55. package/build/types/stream.js +2 -0
  56. package/modules/enums.ts +32 -0
  57. package/modules/errors.ts +24 -9
  58. package/modules/key.ts +4 -1
  59. package/modules/utils.ts +5 -0
  60. package/package.json +4 -1
  61. package/services/activities/activity.ts +34 -8
  62. package/services/activities/await.ts +11 -4
  63. package/services/activities/cycle.ts +10 -3
  64. package/services/activities/hook.ts +8 -3
  65. package/services/activities/index.ts +2 -2
  66. package/services/activities/interrupt.ts +159 -0
  67. package/services/activities/signal.ts +9 -3
  68. package/services/activities/trigger.ts +21 -5
  69. package/services/activities/worker.ts +10 -3
  70. package/services/collator/index.ts +10 -1
  71. package/services/compiler/deployer.ts +1 -3
  72. package/services/connector/index.ts +3 -5
  73. package/services/durable/client.ts +10 -7
  74. package/services/durable/factory.ts +65 -284
  75. package/services/durable/handle.ts +55 -9
  76. package/services/durable/index.ts +11 -0
  77. package/services/durable/meshos.ts +3 -7
  78. package/services/durable/worker.ts +11 -5
  79. package/services/durable/workflow.ts +66 -2
  80. package/services/engine/index.ts +74 -26
  81. package/services/hotmesh/index.ts +28 -6
  82. package/services/quorum/index.ts +9 -0
  83. package/services/signaler/stream.ts +28 -25
  84. package/services/store/index.ts +119 -11
  85. package/services/task/index.ts +18 -18
  86. package/types/activity.ts +38 -8
  87. package/types/durable.ts +8 -4
  88. package/types/index.ts +1 -1
  89. package/types/job.ts +30 -1
  90. package/types/quorum.ts +13 -8
  91. package/types/stream.ts +3 -0
  92. package/build/services/activities/iterate.d.ts +0 -9
  93. package/build/services/activities/iterate.js +0 -13
  94. package/services/activities/iterate.ts +0 -26
@@ -1,15 +1,28 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WorkflowHandleService = void 0;
4
+ const enums_1 = require("../../modules/enums");
4
5
  class WorkflowHandleService {
5
6
  constructor(hotMesh, workflowTopic, workflowId) {
6
7
  this.workflowTopic = workflowTopic;
7
8
  this.workflowId = workflowId;
8
9
  this.hotMesh = hotMesh;
9
10
  }
11
+ /**
12
+ * Sends a signal to the workflow. This is a way to send
13
+ * a message to a workflow that is paused due to having
14
+ * executed a `waitForSignal` workflow extension. Awakens
15
+ * the workflow if no other signals are pending.
16
+ */
10
17
  async signal(signalId, data) {
11
18
  await this.hotMesh.hook(`${this.hotMesh.appId}.wfs.signal`, { id: signalId, data });
12
19
  }
20
+ /**
21
+ * Returns the job state of the workflow. If the workflow has completed
22
+ * this is also the job output. If the workflow is still running, this
23
+ * is the current state of the job, but it may change depending upon
24
+ * the activities that remain.
25
+ */
13
26
  async state(metadata = false) {
14
27
  const state = await this.hotMesh.getState(`${this.hotMesh.appId}.execute`, this.workflowId);
15
28
  if (!state.data && state.metadata.err) {
@@ -17,12 +30,38 @@ class WorkflowHandleService {
17
30
  }
18
31
  return metadata ? state : state.data;
19
32
  }
33
+ /**
34
+ * Returns the current search state of the workflow. This is
35
+ * different than the job state or individual activity state.
36
+ * Search state represents name/value pairs that were added
37
+ * to the workflow. As the workflow is stored in a Redis hash,
38
+ * this is a way to store additional data that is indexed
39
+ * and searchable using the RediSearch module.
40
+ */
20
41
  async queryState(fields) {
21
42
  return await this.hotMesh.getQueryState(this.workflowId, fields);
22
43
  }
44
+ /**
45
+ * Returns the current status of the workflow. This is a semaphore
46
+ * value that represents the current state of the workflow, where
47
+ * 0 is complete and a negative value represents that the flow was
48
+ * interrupted.
49
+ */
23
50
  async status() {
24
51
  return await this.hotMesh.getStatus(this.workflowId);
25
52
  }
53
+ /**
54
+ * Interrupts a running workflow. Standard Job Completion tasks will
55
+ * run. Subscribers will be notified and the job hash will be expired.
56
+ */
57
+ async interrupt(options) {
58
+ return await this.hotMesh.interrupt(`${this.hotMesh.appId}.execute`, this.workflowId, options);
59
+ }
60
+ /**
61
+ * Awaits for the workflow to complete and returns the result. If
62
+ * the workflow thows and error, this method will likewise throw
63
+ * an error.
64
+ */
26
65
  async result(loadState) {
27
66
  if (loadState) {
28
67
  const state = await this.hotMesh.getState(`${this.hotMesh.appId}.execute`, this.workflowId);
@@ -50,30 +89,34 @@ class WorkflowHandleService {
50
89
  }
51
90
  else if (!response) {
52
91
  const state = await this.hotMesh.getState(`${this.hotMesh.appId}.execute`, this.workflowId);
53
- if (!state.data && state.metadata.err) {
54
- return reject(JSON.parse(state.metadata.err));
92
+ if (state.metadata.err) {
93
+ const error = JSON.parse(state.metadata.err);
94
+ if (error.code === enums_1.STATUS_CODE_INTERRUPT || !state.data) {
95
+ return reject({ ...error, job_id: this.workflowId });
96
+ }
55
97
  }
56
98
  response = state.data?.response;
57
99
  }
58
100
  resolve(response);
59
101
  };
60
102
  //check for done
61
- if (status == 0) {
103
+ if (status <= 0) {
62
104
  return complete();
63
105
  }
64
106
  //subscribe to topic
65
107
  this.hotMesh.sub(topic, async (topic, state) => {
66
- if (!state.data && state.metadata.err) {
67
- await complete(null, state.metadata.err);
68
- }
69
- else {
70
- await complete(state.data?.response);
108
+ if (state.metadata.err) {
109
+ const error = JSON.parse(state.metadata.err);
110
+ if (error.code === enums_1.STATUS_CODE_INTERRUPT || !state.data) {
111
+ return await complete(null, state.metadata.err);
112
+ }
71
113
  }
114
+ await complete(state.data?.response);
72
115
  });
73
116
  //resolve for race condition
74
117
  setTimeout(async () => {
75
118
  status = await this.hotMesh.getStatus(this.workflowId);
76
- if (status == 0) {
119
+ if (status <= 0) {
77
120
  await complete();
78
121
  }
79
122
  }, 0);
@@ -12,5 +12,10 @@ export declare const Durable: {
12
12
  MeshOS: typeof MeshOSService;
13
13
  Worker: typeof WorkerService;
14
14
  workflow: typeof WorkflowService;
15
+ /**
16
+ * Shutdown everything. All connections, workers, and clients will be closed.
17
+ * Include in your signal handlers to ensure a clean shutdown.
18
+ */
19
+ shutdown(): Promise<void>;
15
20
  };
16
21
  export type { ContextType };
@@ -7,6 +7,7 @@ const meshos_1 = require("./meshos");
7
7
  const search_1 = require("./search");
8
8
  const worker_1 = require("./worker");
9
9
  const workflow_1 = require("./workflow");
10
+ const hotmesh_1 = require("../hotmesh");
10
11
  exports.Durable = {
11
12
  Client: client_1.ClientService,
12
13
  Connection: connection_1.ConnectionService,
@@ -14,4 +15,13 @@ exports.Durable = {
14
15
  MeshOS: meshos_1.MeshOSService,
15
16
  Worker: worker_1.WorkerService,
16
17
  workflow: workflow_1.WorkflowService,
18
+ /**
19
+ * Shutdown everything. All connections, workers, and clients will be closed.
20
+ * Include in your signal handlers to ensure a clean shutdown.
21
+ */
22
+ async shutdown() {
23
+ await client_1.ClientService.shutdown();
24
+ await worker_1.WorkerService.shutdown();
25
+ await hotmesh_1.HotMeshService.stop();
26
+ }
17
27
  };
@@ -1,14 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MeshOSService = void 0;
4
- const nanoid_1 = require("nanoid");
5
4
  const _1 = require(".");
6
5
  const asyncLocalStorage_1 = require("./asyncLocalStorage");
7
6
  const client_1 = require("./client");
8
7
  const search_1 = require("./search");
9
8
  const worker_1 = require("./worker");
10
9
  const workflow_1 = require("./workflow");
11
- const stream_1 = require("../signaler/stream");
10
+ const utils_1 = require("../../modules/utils");
12
11
  /**
13
12
  * The base class for running MeshOS workflows.
14
13
  * Extend this class, add your Redis config, and add functions to
@@ -32,7 +31,7 @@ class MeshOSService {
32
31
  */
33
32
  static mintGuid() {
34
33
  const my = new this();
35
- return `${my.search?.prefix?.[0]}${(0, nanoid_1.nanoid)()}`;
34
+ return `${my.search?.prefix?.[0]}${(0, utils_1.guid)()}`;
36
35
  }
37
36
  /**
38
37
  * Creates an FT search index
@@ -47,9 +46,7 @@ class MeshOSService {
47
46
  * @returns {Promise<void>}
48
47
  */
49
48
  static async stopWorkers() {
50
- await _1.Durable.Client.shutdown();
51
- await _1.Durable.Worker.shutdown();
52
- await stream_1.StreamSignaler.stopConsuming();
49
+ await _1.Durable.shutdown();
53
50
  }
54
51
  /**
55
52
  * Initializes the worker(s). This is a static
@@ -153,14 +153,21 @@ class WorkerService {
153
153
  //incoming data payload has arguments and workflowId
154
154
  const workflowInput = data.data;
155
155
  const context = new Map();
156
+ context.set('raw', data);
156
157
  context.set('namespace', config.namespace ?? factory_1.APP_ID);
157
158
  context.set('counter', counter);
158
159
  context.set('workflowId', workflowInput.workflowId);
159
- if (data.data.workflowDimension) {
160
+ context.set('workflowId', workflowInput.workflowId);
161
+ if (workflowInput.originJobId) {
162
+ //if present there is an origin job to which this job is subordinated;
163
+ // garbage collect (expire) this job when originJobId is expired
164
+ context.set('originJobId', workflowInput.originJobId);
165
+ }
166
+ if (workflowInput.workflowDimension) {
160
167
  //every hook function runs in an isolated dimension controlled
161
168
  //by the index assigned when the signal was received; even if the
162
169
  //hook function re-runs, its scope will always remain constant
163
- context.set('workflowDimension', data.data.workflowDimension);
170
+ context.set('workflowDimension', workflowInput.workflowDimension);
164
171
  }
165
172
  context.set('workflowTopic', workflowTopic);
166
173
  context.set('workflowName', workflowTopic.split('-').pop());
@@ -240,9 +247,8 @@ class WorkerService {
240
247
  };
241
248
  }
242
249
  static async shutdown() {
243
- for (const [key, value] of WorkerService.instances) {
244
- const hotMesh = await value;
245
- await hotMesh.stop();
250
+ for (const [_, hotMeshInstance] of WorkerService.instances) {
251
+ (await hotMeshInstance).stop();
246
252
  }
247
253
  }
248
254
  }
@@ -1,6 +1,7 @@
1
1
  import { Search } from './search';
2
2
  import { HotMeshService as HotMesh } from '../hotmesh';
3
3
  import { ActivityConfig, HookOptions, ProxyType, WorkflowContext, WorkflowOptions } from "../../types/durable";
4
+ import { JobInterruptOptions } from '../../types/job';
4
5
  export declare class WorkflowService {
5
6
  /**
6
7
  * Spawns a child workflow. await and return the result.
@@ -80,6 +81,29 @@ export declare class WorkflowService {
80
81
  * @param {HookOptions} options - the hook options
81
82
  */
82
83
  static hook(options: HookOptions): Promise<string>;
84
+ static getLocalState(): {
85
+ workflowId: any;
86
+ namespace: any;
87
+ workflowTopic: any;
88
+ workflowDimension: any;
89
+ counter: any;
90
+ };
91
+ /**
92
+ * Executes a function once and caches the result. If the function is called
93
+ * again, the cached result is returned. This is useful for wrapping
94
+ * expensive activity calls that should only be run once, but which might
95
+ * not require the configuration nuance/expense provided by proxyActivities.
96
+ * @template T - the result type
97
+ */
98
+ static once<T>(fn: (...args: any[]) => Promise<T>, ...args: any[]): Promise<T>;
99
+ /**
100
+ * Interrupts a running job
101
+ *
102
+ * @param {string} jobId - the target job id
103
+ * @param {JobInterruptOptions} options - the interrupt options
104
+ * @returns {Promise<string>} - the stream id
105
+ */
106
+ static interrupt(jobId: string, options?: JobInterruptOptions): Promise<string | void>;
83
107
  /**
84
108
  * Sleeps the workflow for a duration. As the function is reentrant,
85
109
  * upon reentry, the function will traverse prior execution paths up
@@ -26,6 +26,7 @@ class WorkflowService {
26
26
  const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
27
27
  const namespace = store.get('namespace');
28
28
  const workflowId = store.get('workflowId');
29
+ const originJobId = store.get('originJobId');
29
30
  const workflowDimension = store.get('workflowDimension') ?? '';
30
31
  const workflowTrace = store.get('workflowTrace');
31
32
  const workflowSpan = store.get('workflowSpan');
@@ -49,6 +50,7 @@ class WorkflowService {
49
50
  ...options,
50
51
  namespace,
51
52
  workflowId: childJobId,
53
+ originJobId: originJobId ?? workflowId,
52
54
  parentWorkflowId,
53
55
  workflowTrace,
54
56
  workflowSpan,
@@ -261,6 +263,57 @@ class WorkflowService {
261
263
  return await hotMeshClient.hook(`${namespace}.flow.signal`, payload, stream_1.StreamStatus.PENDING, 202);
262
264
  }
263
265
  }
266
+ static getLocalState() {
267
+ const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
268
+ return {
269
+ workflowId: store.get('workflowId'),
270
+ namespace: store.get('namespace'),
271
+ workflowTopic: store.get('workflowTopic'),
272
+ workflowDimension: store.get('workflowDimension') ?? '',
273
+ counter: store.get('counter'),
274
+ };
275
+ }
276
+ /**
277
+ * Executes a function once and caches the result. If the function is called
278
+ * again, the cached result is returned. This is useful for wrapping
279
+ * expensive activity calls that should only be run once, but which might
280
+ * not require the configuration nuance/expense provided by proxyActivities.
281
+ * @template T - the result type
282
+ */
283
+ static async once(fn, ...args) {
284
+ const { workflowId, namespace, workflowTopic, workflowDimension, counter: COUNTER, } = WorkflowService.getLocalState();
285
+ const execIndex = COUNTER.counter = COUNTER.counter + 1;
286
+ const sessionId = `-once${workflowDimension}-${execIndex}-`;
287
+ const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
288
+ const keyParams = {
289
+ appId: hotMeshClient.appId,
290
+ jobId: workflowId
291
+ };
292
+ const workflowGuid = key_1.KeyService.mintKey(hotMeshClient.namespace, key_1.KeyType.JOB_STATE, keyParams);
293
+ const value = await hotMeshClient.engine.store.exec('HGET', workflowGuid, sessionId);
294
+ if (value) {
295
+ return JSON.parse(value);
296
+ }
297
+ const response = await fn(...args);
298
+ await hotMeshClient.engine.store.exec('HSET', workflowGuid, sessionId, JSON.stringify(response));
299
+ return response;
300
+ }
301
+ /**
302
+ * Interrupts a running job
303
+ *
304
+ * @param {string} jobId - the target job id
305
+ * @param {JobInterruptOptions} options - the interrupt options
306
+ * @returns {Promise<string>} - the stream id
307
+ */
308
+ static async interrupt(jobId, options = {}) {
309
+ const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
310
+ const workflowTopic = store.get('workflowTopic');
311
+ const namespace = store.get('namespace');
312
+ const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
313
+ if (await WorkflowService.isSideEffectAllowed(hotMeshClient, 'interrupt')) {
314
+ return await hotMeshClient.interrupt(`${hotMeshClient.appId}.execute`, jobId, options);
315
+ }
316
+ }
264
317
  /**
265
318
  * Sleeps the workflow for a duration. As the function is reentrant,
266
319
  * upon reentry, the function will traverse prior execution paths up
@@ -379,6 +432,7 @@ class WorkflowService {
379
432
  //increment by state (not value) to avoid race conditions
380
433
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
381
434
  const workflowId = store.get('workflowId');
435
+ const originJobId = store.get('originJobId');
382
436
  const workflowDimension = store.get('workflowDimension') ?? '';
383
437
  const workflowTopic = store.get('workflowTopic');
384
438
  const trc = store.get('workflowTrace');
@@ -412,7 +466,8 @@ class WorkflowService {
412
466
  const duration = (0, ms_1.default)(options?.startToCloseTimeout || '1 minute');
413
467
  const payload = {
414
468
  arguments: Array.from(arguments),
415
- //the parent id is provided to categorize this activity for later cleanup
469
+ //when the origin job is removed
470
+ originJobId: originJobId ?? workflowId,
416
471
  parentWorkflowId: `${workflowId}-a`,
417
472
  workflowId: activityJobId,
418
473
  workflowTopic: activityTopic,
@@ -1,6 +1,7 @@
1
1
  import { Await } from '../activities/await';
2
2
  import { Cycle } from '../activities/cycle';
3
3
  import { Hook } from '../activities/hook';
4
+ import { Interrupt } from '../activities/interrupt';
4
5
  import { Signal } from '../activities/signal';
5
6
  import { Worker } from '../activities/worker';
6
7
  import { Trigger } from '../activities/trigger';
@@ -14,7 +15,7 @@ import { TaskService } from '../task';
14
15
  import { AppVID } from '../../types/app';
15
16
  import { ActivityType } from '../../types/activity';
16
17
  import { CacheMode } from '../../types/cache';
17
- import { JobState, JobData, JobMetadata, JobOutput, JobStatus } from '../../types/job';
18
+ import { JobState, JobData, JobMetadata, JobOutput, JobStatus, JobInterruptOptions, JobCompletionOptions } from '../../types/job';
18
19
  import { HotMeshApps, HotMeshConfig, HotMeshManifest, HotMeshSettings } from '../../types/hotmesh';
19
20
  import { JobMessageCallback } from '../../types/quorum';
20
21
  import { RedisClient, RedisMulti } from '../../types/redis';
@@ -50,7 +51,7 @@ declare class EngineService {
50
51
  processWebHooks(): Promise<void>;
51
52
  processTimeHooks(): Promise<void>;
52
53
  throttle(delayInMillis: number): Promise<void>;
53
- initActivity(topic: string, data?: JobData, context?: JobState): Promise<Await | Cycle | Hook | Signal | Trigger | Worker>;
54
+ initActivity(topic: string, data?: JobData, context?: JobState): Promise<Await | Cycle | Hook | Signal | Trigger | Worker | Interrupt>;
54
55
  getSchema(topic: string): Promise<[activityId: string, schema: ActivityType]>;
55
56
  getSettings(): Promise<HotMeshSettings>;
56
57
  isPrivate(topic: string): boolean;
@@ -63,9 +64,10 @@ declare class EngineService {
63
64
  execAdjacentParent(context: JobState, jobOutput: JobOutput, emit?: boolean): Promise<string>;
64
65
  hasParentJob(context: JobState): boolean;
65
66
  resolveError(metadata: JobMetadata): StreamError | undefined;
67
+ interrupt(topic: string, jobId: string, options?: JobInterruptOptions): Promise<string>;
66
68
  scrub(jobId: string): Promise<void>;
67
69
  hook(topic: string, data: JobData, status?: StreamStatus, code?: StreamCode): Promise<string>;
68
- hookTime(jobId: string, activityId: string): Promise<JobStatus | void>;
70
+ hookTime(jobId: string, activityId: string, type?: 'sleep' | 'expire' | 'interrupt'): Promise<string | void>;
69
71
  hookAll(hookTopic: string, data: JobData, keyResolver: JobStatsInput, queryFacets?: string[]): Promise<string[]>;
70
72
  pub(topic: string, data: JobData, context?: JobState): Promise<string>;
71
73
  sub(topic: string, callback: JobMessageCallback): Promise<void>;
@@ -73,14 +75,20 @@ declare class EngineService {
73
75
  psub(wild: string, callback: JobMessageCallback): Promise<void>;
74
76
  punsub(wild: string): Promise<void>;
75
77
  pubsub(topic: string, data: JobData, context?: JobState | null, timeout?: number): Promise<JobOutput>;
76
- resolveOneTimeSubscription(context: JobState, jobOutput: JobOutput, emit?: boolean): Promise<void>;
78
+ pubOneTimeSubs(context: JobState, jobOutput: JobOutput, emit?: boolean): Promise<void>;
77
79
  getPublishesTopic(context: JobState): Promise<string>;
78
- resolvePersistentSubscriptions(context: JobState, jobOutput: JobOutput, emit?: boolean): Promise<void>;
80
+ pubPermSubs(context: JobState, jobOutput: JobOutput, emit?: boolean): Promise<void>;
79
81
  add(streamData: StreamData | StreamDataResponse): Promise<string>;
80
82
  registerJobCallback(jobId: string, jobCallback: JobMessageCallback): void;
81
83
  delistJobCallback(jobId: string): void;
82
84
  hasOneTimeSubscription(context: JobState): boolean;
83
- runJobCompletionTasks(context: JobState, emit?: boolean): Promise<void>;
85
+ runJobCompletionTasks(context: JobState, options?: JobCompletionOptions): Promise<string | void>;
86
+ /**
87
+ * Job hash expiration is typically reliant on the metadata field
88
+ * if the activity concludes normally. However, if the job is `interrupted`,
89
+ * it will be expired immediately.
90
+ */
91
+ resolveExpires(context: JobState, options: JobCompletionOptions): number;
84
92
  getStatus(jobId: string): Promise<JobStatus>;
85
93
  getState(topic: string, jobId: string): Promise<JobOutput>;
86
94
  getQueryState(jobId: string, fields: string[]): Promise<StringAnyType>;
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.EngineService = void 0;
7
7
  const key_1 = require("../../modules/key");
8
+ const enums_1 = require("../../modules/enums");
8
9
  const utils_1 = require("../../modules/utils");
9
10
  const activities_1 = __importDefault(require("../activities"));
10
11
  const compiler_1 = require("../compiler");
@@ -20,11 +21,6 @@ const ioredis_3 = require("../sub/clients/ioredis");
20
21
  const redis_3 = require("../sub/clients/redis");
21
22
  const task_1 = require("../task");
22
23
  const stream_2 = require("../../types/stream");
23
- //wait time to see if a job is complete
24
- const OTT_WAIT_TIME = 1000;
25
- const STATUS_CODE_SUCCESS = 200;
26
- const STATUS_CODE_PENDING = 202;
27
- const STATUS_CODE_TIMEOUT = 504;
28
24
  class EngineService {
29
25
  constructor() {
30
26
  this.cacheMode = 'cache';
@@ -230,6 +226,7 @@ class EngineService {
230
226
  aid: streamData.metadata.aid,
231
227
  status: streamData.status || stream_2.StreamStatus.SUCCESS,
232
228
  code: streamData.code || 200,
229
+ type: streamData.type,
233
230
  });
234
231
  const context = {
235
232
  metadata: {
@@ -285,6 +282,7 @@ class EngineService {
285
282
  const spn = context['$self']?.output?.metadata?.l2s || context['$self']?.output?.metadata?.l1s;
286
283
  const streamData = {
287
284
  metadata: {
285
+ guid: (0, utils_1.guid)(),
288
286
  jid: context.metadata.pj,
289
287
  dad: context.metadata.pd,
290
288
  aid: context.metadata.pa,
@@ -301,17 +299,16 @@ class EngineService {
301
299
  }
302
300
  else if (emit) {
303
301
  streamData.status = stream_2.StreamStatus.PENDING;
304
- streamData.code = STATUS_CODE_PENDING;
302
+ streamData.code = enums_1.STATUS_CODE_PENDING;
305
303
  }
306
304
  else {
307
305
  streamData.status = stream_2.StreamStatus.SUCCESS;
308
- streamData.code = STATUS_CODE_SUCCESS;
306
+ streamData.code = enums_1.STATUS_CODE_SUCCESS;
309
307
  }
310
308
  return (await this.streamSignaler?.publishMessage(null, streamData));
311
309
  }
312
310
  }
313
311
  hasParentJob(context) {
314
- //todo: include the dimensional address (pd)
315
312
  return Boolean(context.metadata.pj && context.metadata.pa);
316
313
  }
317
314
  resolveError(metadata) {
@@ -319,8 +316,19 @@ class EngineService {
319
316
  return JSON.parse(metadata.err);
320
317
  }
321
318
  }
319
+ // ****************** `INTERRUPT` ACTIVE JOBS *****************
320
+ async interrupt(topic, jobId, options = {}) {
321
+ await this.store.interrupt(topic, jobId, options);
322
+ const context = await this.getState(topic, jobId);
323
+ const completionOpts = {
324
+ interrupt: options.descend,
325
+ expire: options.expire,
326
+ };
327
+ return await this.runJobCompletionTasks(context, completionOpts);
328
+ }
322
329
  // ****************** `SCRUB` CLEAN COMPLETED JOBS *****************
323
330
  async scrub(jobId) {
331
+ //todo: do not allow scrubbing of non-existent or actively running job
324
332
  await this.store.scrub(jobId);
325
333
  }
326
334
  // ****************** `HOOK` ACTIVITY RE-ENTRY POINT *****************
@@ -332,6 +340,7 @@ class EngineService {
332
340
  status,
333
341
  code,
334
342
  metadata: {
343
+ guid: (0, utils_1.guid)(),
335
344
  aid,
336
345
  topic
337
346
  },
@@ -339,13 +348,21 @@ class EngineService {
339
348
  };
340
349
  return await this.streamSignaler.publishMessage(null, streamData);
341
350
  }
342
- async hookTime(jobId, activityId) {
343
- //the activityid is concatenated with its dimensional address (dad); split to resolve
351
+ async hookTime(jobId, activityId, type) {
352
+ if (type === 'interrupt') {
353
+ return await this.interrupt(activityId, //note: 'activityId' is the actually job topic
354
+ jobId, { suppress: true, expire: 1 });
355
+ }
356
+ else if (type === 'expire') {
357
+ return await this.store.expireJob(jobId, 1);
358
+ }
359
+ //'sleep': parse the activityId into parts
344
360
  const [aid, ...dimensions] = activityId.split(',');
345
361
  const dad = `,${dimensions.join(',')}`;
346
362
  const streamData = {
347
363
  type: stream_2.StreamDataType.TIMEHOOK,
348
364
  metadata: {
365
+ guid: (0, utils_1.guid)(),
349
366
  jid: jobId,
350
367
  aid,
351
368
  dad,
@@ -407,7 +424,7 @@ class EngineService {
407
424
  return await this.subscribe.punsubscribe(key_1.KeyType.QUORUM, this.appId, wild);
408
425
  }
409
426
  //publish and await (returns the job and data (if ready)); throws error with jobid if not
410
- async pubsub(topic, data, context, timeout = OTT_WAIT_TIME) {
427
+ async pubsub(topic, data, context, timeout = enums_1.OTT_WAIT_TIME) {
411
428
  context = {
412
429
  metadata: {
413
430
  ngn: this.guid,
@@ -432,14 +449,14 @@ class EngineService {
432
449
  setTimeout(() => {
433
450
  this.delistJobCallback(jobId);
434
451
  reject({
435
- code: STATUS_CODE_TIMEOUT,
452
+ code: enums_1.STATUS_CODE_TIMEOUT,
436
453
  message: 'timeout',
437
454
  job_id: jobId
438
455
  });
439
456
  }, timeout);
440
457
  });
441
458
  }
442
- async resolveOneTimeSubscription(context, jobOutput, emit = false) {
459
+ async pubOneTimeSubs(context, jobOutput, emit = false) {
443
460
  //todo: subscriber should query for the job...only publish minimum context needed
444
461
  if (this.hasOneTimeSubscription(context)) {
445
462
  const message = {
@@ -456,7 +473,7 @@ class EngineService {
456
473
  const schema = await this.store.getSchema(activityId, config);
457
474
  return schema.publishes;
458
475
  }
459
- async resolvePersistentSubscriptions(context, jobOutput, emit = false) {
476
+ async pubPermSubs(context, jobOutput, emit = false) {
460
477
  const topic = await this.getPublishesTopic(context);
461
478
  if (topic) {
462
479
  const message = {
@@ -480,22 +497,30 @@ class EngineService {
480
497
  return Boolean(context.metadata.ngn);
481
498
  }
482
499
  // ********** JOB COMPLETION/CLEANUP (AND JOB EMIT) ***********
483
- async runJobCompletionTasks(context, emit = false) {
484
- //if 'emit' is true, the job isn't done. it's just emitting
500
+ async runJobCompletionTasks(context, options = {}) {
501
+ //'emit' indicates the job is still active
485
502
  const isAwait = this.hasParentJob(context);
486
- const isOneTimeSubscription = this.hasOneTimeSubscription(context);
503
+ const isOneTimeSub = this.hasOneTimeSubscription(context);
487
504
  const topic = await this.getPublishesTopic(context);
488
- if (isAwait || isOneTimeSubscription || topic) {
505
+ let msgId;
506
+ if (isAwait || isOneTimeSub || topic) {
489
507
  const jobOutput = await this.getState(context.metadata.tpc, context.metadata.jid);
490
- //always wait for stream pub/sub
491
- await this.execAdjacentParent(context, jobOutput, emit);
492
- //no need to wait for standard pub/sub
493
- this.resolveOneTimeSubscription(context, jobOutput, emit);
494
- this.resolvePersistentSubscriptions(context, jobOutput, emit);
495
- }
496
- if (!emit) {
497
- this.task.registerJobForCleanup(context.metadata.jid, context.metadata.expire);
498
- }
508
+ msgId = await this.execAdjacentParent(context, jobOutput, options.emit);
509
+ this.pubOneTimeSubs(context, jobOutput, options.emit);
510
+ this.pubPermSubs(context, jobOutput, options.emit);
511
+ }
512
+ if (!options.emit) {
513
+ this.task.registerJobForCleanup(context.metadata.jid, this.resolveExpires(context, options), options);
514
+ }
515
+ return msgId;
516
+ }
517
+ /**
518
+ * Job hash expiration is typically reliant on the metadata field
519
+ * if the activity concludes normally. However, if the job is `interrupted`,
520
+ * it will be expired immediately.
521
+ */
522
+ resolveExpires(context, options) {
523
+ return context.metadata.expire ?? options.expire ?? enums_1.DURABLE_EXPIRE_SECONDS;
499
524
  }
500
525
  // ****** GET JOB STATE/COLLATION STATUS BY ID *********
501
526
  async getStatus(jobId) {
@@ -2,7 +2,7 @@ import { EngineService } from '../engine';
2
2
  import { ILogger } from '../logger';
3
3
  import { QuorumService } from '../quorum';
4
4
  import { WorkerService } from '../worker';
5
- import { JobState, JobData, JobOutput, JobStatus } from '../../types/job';
5
+ import { JobState, JobData, JobOutput, JobStatus, JobInterruptOptions } from '../../types/job';
6
6
  import { HotMeshConfig, HotMeshManifest } from '../../types/hotmesh';
7
7
  import { JobMessageCallback } from '../../types/quorum';
8
8
  import { JobStatsInput, GetStatsOptions, IdsResponse, StatsResponse } from '../../types/stats';
@@ -16,6 +16,7 @@ declare class HotMeshService {
16
16
  quorum: QuorumService | null;
17
17
  workers: WorkerService[];
18
18
  logger: ILogger;
19
+ static disconnecting: boolean;
19
20
  verifyAndSetNamespace(namespace?: string): void;
20
21
  verifyAndSetAppId(appId: string): void;
21
22
  static init(config: HotMeshConfig): Promise<HotMeshService>;
@@ -33,16 +34,19 @@ declare class HotMeshService {
33
34
  plan(path: string): Promise<HotMeshManifest>;
34
35
  deploy(pathOrYAML: string): Promise<HotMeshManifest>;
35
36
  activate(version: string, delay?: number): Promise<boolean>;
37
+ inventory(version: string, delay?: number): Promise<number>;
36
38
  getStats(topic: string, query: JobStatsInput): Promise<StatsResponse>;
37
39
  getStatus(jobId: string): Promise<JobStatus>;
38
40
  getState(topic: string, jobId: string): Promise<JobOutput>;
39
41
  getQueryState(jobId: string, fields: string[]): Promise<StringAnyType>;
40
42
  getIds(topic: string, query: JobStatsInput, queryFacets?: any[]): Promise<IdsResponse>;
41
43
  resolveQuery(topic: string, query: JobStatsInput): Promise<GetStatsOptions>;
44
+ interrupt(topic: string, jobId: string, options?: JobInterruptOptions): Promise<string>;
42
45
  scrub(jobId: string): Promise<void>;
43
46
  hook(topic: string, data: JobData, status?: StreamStatus, code?: StreamCode): Promise<string>;
44
47
  hookAll(hookTopic: string, data: JobData, query: JobStatsInput, queryFacets?: string[]): Promise<string[]>;
45
- stop(): Promise<void>;
48
+ static stop(): Promise<void>;
49
+ stop(): void;
46
50
  compress(terms: string[]): Promise<boolean>;
47
51
  }
48
52
  export { HotMeshService };