@hotmeshio/hotmesh 0.0.41 → 0.0.42

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.
@@ -23,13 +23,6 @@ declare class DurableWaitForSignalError extends Error {
23
23
  index: number;
24
24
  }[]);
25
25
  }
26
- declare class DurableSleepError extends Error {
27
- code: number;
28
- duration: number;
29
- index: number;
30
- dimension: string;
31
- constructor(message: string, duration: number, index: number, dimension: string);
32
- }
33
26
  declare class DurableSleepForError extends Error {
34
27
  code: number;
35
28
  duration: number;
@@ -86,4 +79,4 @@ declare class CollationError extends Error {
86
79
  fault: CollationFaultType;
87
80
  constructor(status: number, leg: ActivityDuplex, stage: CollationStage, fault?: CollationFaultType);
88
81
  }
89
- export { CollationError, DurableFatalError, DurableIncompleteSignalError, DurableMaxedError, DurableRetryError, DurableSleepError, DurableSleepForError, DurableTimeoutError, DurableWaitForSignalError, DuplicateJobError, ExecActivityError, GenerationalError, GetStateError, InactiveJobError, MapDataError, RegisterTimeoutError, SetStateError, };
82
+ export { CollationError, DurableFatalError, DurableIncompleteSignalError, DurableMaxedError, DurableRetryError, DurableSleepForError, DurableTimeoutError, DurableWaitForSignalError, DuplicateJobError, ExecActivityError, GenerationalError, GetStateError, InactiveJobError, MapDataError, RegisterTimeoutError, SetStateError, };
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SetStateError = exports.RegisterTimeoutError = exports.MapDataError = exports.InactiveJobError = exports.GetStateError = exports.GenerationalError = exports.ExecActivityError = exports.DuplicateJobError = exports.DurableWaitForSignalError = exports.DurableTimeoutError = exports.DurableSleepForError = exports.DurableSleepError = exports.DurableRetryError = exports.DurableMaxedError = exports.DurableIncompleteSignalError = exports.DurableFatalError = exports.CollationError = void 0;
3
+ exports.SetStateError = exports.RegisterTimeoutError = exports.MapDataError = exports.InactiveJobError = exports.GetStateError = exports.GenerationalError = exports.ExecActivityError = exports.DuplicateJobError = exports.DurableWaitForSignalError = exports.DurableTimeoutError = exports.DurableSleepForError = exports.DurableRetryError = exports.DurableMaxedError = exports.DurableIncompleteSignalError = exports.DurableFatalError = exports.CollationError = void 0;
4
4
  const enums_1 = require("./enums");
5
5
  class GetStateError extends Error {
6
6
  constructor(jobId) {
@@ -34,17 +34,6 @@ class DurableWaitForSignalError extends Error {
34
34
  }
35
35
  }
36
36
  exports.DurableWaitForSignalError = DurableWaitForSignalError;
37
- /* @deprecated */
38
- class DurableSleepError extends Error {
39
- constructor(message, duration, index, dimension) {
40
- super(message);
41
- this.duration = duration;
42
- this.index = index;
43
- this.dimension = dimension;
44
- this.code = 595;
45
- }
46
- }
47
- exports.DurableSleepError = DurableSleepError;
48
37
  class DurableSleepForError extends Error {
49
38
  constructor(message, duration, index, dimension) {
50
39
  super(message);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.41",
3
+ "version": "0.0.42",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -31,6 +31,11 @@ declare class Activity {
31
31
  * all aspects of the entry including job and activty state
32
32
  */
33
33
  verifyEntry(): Promise<void>;
34
+ /**
35
+ * Upon entering leg 2 of a duplexed activty, verify
36
+ * all aspects of the re-entry including job and activty state
37
+ */
38
+ verifyReentry(): Promise<number>;
34
39
  processEvent(status?: StreamStatus, code?: StreamCode, type?: 'hook' | 'output'): Promise<void>;
35
40
  processPending(telemetry: TelemetryService, type: 'hook' | 'output'): Promise<MultiResponseFlags>;
36
41
  processSuccess(telemetry: TelemetryService, type: 'hook' | 'output'): Promise<MultiResponseFlags>;
@@ -57,7 +62,9 @@ declare class Activity {
57
62
  getState(): Promise<void>;
58
63
  /**
59
64
  * if the job is created/deleted/created with the same key,
60
- * the 'gid' ensures no stale messages enter the stream
65
+ * the 'gid' ensures no stale messages (such as sleep delays)
66
+ * enter the workstream. Any message with a mismatched gid
67
+ * belongs to a prior job and can safely be ignored/dropped.
61
68
  */
62
69
  assertGenerationalId(jobGID: string, msgGID?: string): void;
63
70
  initDimensionalAddress(dad: string): void;
@@ -40,6 +40,17 @@ class Activity {
40
40
  collator_1.CollatorService.assertJobActive(this.context.metadata.js, this.context.metadata.jid, this.metadata.aid);
41
41
  await collator_1.CollatorService.notarizeEntry(this);
42
42
  }
43
+ /**
44
+ * Upon entering leg 2 of a duplexed activty, verify
45
+ * all aspects of the re-entry including job and activty state
46
+ */
47
+ async verifyReentry() {
48
+ const guid = this.context.metadata.guid;
49
+ this.setLeg(2);
50
+ await this.getState();
51
+ collator_1.CollatorService.assertJobActive(this.context.metadata.js, this.context.metadata.jid, this.metadata.aid);
52
+ return await collator_1.CollatorService.notarizeReentry(this, guid);
53
+ }
43
54
  //******** DUPLEX RE-ENTRY POINT ********//
44
55
  async processEvent(status = stream_1.StreamStatus.SUCCESS, code = 200, type = 'output') {
45
56
  this.setLeg(2);
@@ -54,17 +65,9 @@ class Activity {
54
65
  this.logger.debug('activity-process-event', { topic: this.config.subtype, jid, aid, status, code });
55
66
  let telemetry;
56
67
  try {
57
- await this.getState();
58
- collator_1.CollatorService.assertJobActive(this.context.metadata.js, this.context.metadata.jid, this.metadata.aid);
59
- const aState = await collator_1.CollatorService.notarizeReentry(this);
60
- this.adjacentIndex = collator_1.CollatorService.getDimensionalIndex(aState);
68
+ const collationKey = await this.verifyReentry();
69
+ this.adjacentIndex = collator_1.CollatorService.getDimensionalIndex(collationKey);
61
70
  telemetry = new telemetry_1.TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
62
- let isComplete = collator_1.CollatorService.isActivityComplete(this.context.metadata.js);
63
- if (isComplete) {
64
- this.logger.warn('activity-process-event-duplicate', { jid, aid });
65
- this.logger.debug('activity-process-event-duplicate-resolution', { resolution: 'Increase HotMesh config `reclaimDelay` timeout.' });
66
- return;
67
- }
68
71
  telemetry.startActivitySpan(this.leg);
69
72
  let multiResponse;
70
73
  if (status === stream_1.StreamStatus.PENDING) {
@@ -306,7 +309,7 @@ class Activity {
306
309
  telemetry_1.TelemetryService.addTargetTelemetryPaths(consumes, this.config, this.metadata, this.leg);
307
310
  let { dad, jid } = this.context.metadata;
308
311
  const dIds = collator_1.CollatorService.getDimensionsById([...this.config.ancestors, this.metadata.aid], dad || '');
309
- //`state` is a flat hash; context is a tree
312
+ //`state` is a unidimensional hash; context is a tree
310
313
  const [state, status] = await this.store.getState(jid, consumes, dIds);
311
314
  this.context = (0, utils_1.restoreHierarchy)(state);
312
315
  this.assertGenerationalId(this.context.metadata.gid, gid);
@@ -316,7 +319,9 @@ class Activity {
316
319
  }
317
320
  /**
318
321
  * if the job is created/deleted/created with the same key,
319
- * the 'gid' ensures no stale messages enter the stream
322
+ * the 'gid' ensures no stale messages (such as sleep delays)
323
+ * enter the workstream. Any message with a mismatched gid
324
+ * belongs to a prior job and can safely be ignored/dropped.
320
325
  */
321
326
  assertGenerationalId(jobGID, msgGID) {
322
327
  if (msgGID !== jobGID) {
@@ -30,7 +30,17 @@ declare class CollatorService {
30
30
  static authorizeReentry(activity: Activity, multi?: RedisMulti): Promise<number>;
31
31
  static notarizeEarlyExit(activity: Activity, multi?: RedisMulti): Promise<number>;
32
32
  static notarizeEarlyCompletion(activity: Activity, multi?: RedisMulti): Promise<number>;
33
- static notarizeReentry(activity: Activity, multi?: RedisMulti): Promise<number>;
33
+ /**
34
+ * verifies both the concrete and synthetic keys for the activity; concrete keys
35
+ * exist in the original model and are effectively the 'real' keys. In reality,
36
+ * hook activities are atomized during compilation to create a synthetic DAG that
37
+ * is used to track the status of the graph in a distributed environment. The
38
+ * synthetic key represents different dimensional realities and is used to
39
+ * track re-entry overages (it distinguishes between the original and re-entry).
40
+ * The essential challenge is: is this a re-entry that is purposeful in
41
+ * order to induce cycles, or is the re-entry due to a failure in the system?
42
+ */
43
+ static notarizeReentry(activity: Activity, guid: string, multi?: RedisMulti): Promise<number>;
34
44
  static notarizeContinuation(activity: Activity, multi?: RedisMulti): Promise<number>;
35
45
  static notarizeCompletion(activity: Activity, multi?: RedisMulti): Promise<number>;
36
46
  static getDigitAtIndex(num: number, targetDigitIndex: number): number | null;
@@ -38,6 +48,15 @@ declare class CollatorService {
38
48
  static isDuplicate(num: number, targetDigitIndex: number): boolean;
39
49
  static isInactive(num: number): boolean;
40
50
  static isPrimed(amount: number, leg: ActivityDuplex): boolean;
51
+ /**
52
+ * During compilation, the graphs are compiled into structures necessary
53
+ * for distributed processing; these are referred to as 'synthetic DAGs',
54
+ * because they are not part of the original graph, but are used to track
55
+ * the status of the graph in a distributed environment. This check ensures
56
+ * that the 'synthetic key' is not a duplicate. (which is different than
57
+ * saying the 'key' is not a duplicate)
58
+ */
59
+ static verifySyntheticInteger(amount: number): void;
41
60
  static verifyInteger(amount: number, leg: ActivityDuplex, stage: CollationStage): void;
42
61
  static getDimensionsById(ancestors: string[], dad: string): Record<string, string>;
43
62
  /**
@@ -72,7 +91,6 @@ declare class CollatorService {
72
91
  *
73
92
  */
74
93
  static bindAncestorArray(graphs: HotMeshGraph[]): void;
75
- static isActivityComplete(status: number): boolean;
76
94
  /**
77
95
  * All activities exist on a dimensional plane. Zero
78
96
  * is the default. A value of
@@ -67,11 +67,28 @@ class CollatorService {
67
67
  return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1000001 - decrement, this.getDimensionalAddress(activity), multi);
68
68
  }
69
69
  ;
70
- static async notarizeReentry(activity, multi) {
70
+ /**
71
+ * verifies both the concrete and synthetic keys for the activity; concrete keys
72
+ * exist in the original model and are effectively the 'real' keys. In reality,
73
+ * hook activities are atomized during compilation to create a synthetic DAG that
74
+ * is used to track the status of the graph in a distributed environment. The
75
+ * synthetic key represents different dimensional realities and is used to
76
+ * track re-entry overages (it distinguishes between the original and re-entry).
77
+ * The essential challenge is: is this a re-entry that is purposeful in
78
+ * order to induce cycles, or is the re-entry due to a failure in the system?
79
+ */
80
+ static async notarizeReentry(activity, guid, multi) {
81
+ const jid = activity.context.metadata.jid;
82
+ const localMulti = multi || activity.store.getMulti();
71
83
  //increment by 1_000_000 (indicates re-entry and is used to drive the 'dimensional address' for adjacent activities (minus 1))
72
- const amount = await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1000000, this.getDimensionalAddress(activity, true), multi);
73
- this.verifyInteger(amount, 2, 'enter');
74
- return amount;
84
+ await activity.store.collate(jid, activity.metadata.aid, 1000000, this.getDimensionalAddress(activity, true), localMulti);
85
+ await activity.store.collateSynthetic(jid, guid, 1000000, localMulti);
86
+ const [_amountConcrete, _amountSynthetic] = await localMulti.exec();
87
+ const amountConcrete = Array.isArray(_amountConcrete) ? _amountConcrete[1] : _amountConcrete;
88
+ const amountSynthetic = Array.isArray(_amountSynthetic) ? _amountSynthetic[1] : _amountSynthetic;
89
+ this.verifyInteger(amountConcrete, 2, 'enter');
90
+ this.verifySyntheticInteger(amountSynthetic);
91
+ return amountConcrete;
75
92
  }
76
93
  ;
77
94
  static async notarizeContinuation(activity, multi) {
@@ -119,6 +136,26 @@ class CollatorService {
119
136
  this.getDigitAtIndex(amount, 1) < 9;
120
137
  }
121
138
  }
139
+ /**
140
+ * During compilation, the graphs are compiled into structures necessary
141
+ * for distributed processing; these are referred to as 'synthetic DAGs',
142
+ * because they are not part of the original graph, but are used to track
143
+ * the status of the graph in a distributed environment. This check ensures
144
+ * that the 'synthetic key' is not a duplicate. (which is different than
145
+ * saying the 'key' is not a duplicate)
146
+ */
147
+ static verifySyntheticInteger(amount) {
148
+ const samount = amount.toString();
149
+ const isCompletedValue = parseInt(samount[samount.length - 1], 10);
150
+ if (isCompletedValue > 0) {
151
+ //already done error (ack/delete clearly failed; this is a duplicate)
152
+ throw new errors_1.CollationError(amount, 2, 'enter', collator_1.CollationFaultType.INACTIVE);
153
+ }
154
+ else if (amount >= 2000000) {
155
+ //duplicate synthetic key (todo: need to resolve/fix this!!)
156
+ throw new errors_1.CollationError(amount, 2, 'enter', collator_1.CollationFaultType.DUPLICATE);
157
+ }
158
+ }
122
159
  static verifyInteger(amount, leg, stage) {
123
160
  let faultType;
124
161
  if (leg === 1 && stage === 'enter') {
@@ -222,9 +259,6 @@ class CollatorService {
222
259
  dfs(startingNode, []);
223
260
  });
224
261
  }
225
- static isActivityComplete(status) {
226
- return (status - 0) <= 0;
227
- }
228
262
  /**
229
263
  * All activities exist on a dimensional plane. Zero
230
264
  * is the default. A value of
@@ -7,7 +7,6 @@
7
7
  * ERROR CODES:
8
8
  * 594: waitforsignal
9
9
  * 592: sleepFor
10
- * 595: sleep (deprecated)
11
10
  * 596, 597, 598: fatal
12
11
  * 599: retry
13
12
  */
@@ -10,7 +10,6 @@ exports.DEFAULT_COEFFICIENT = exports.APP_ID = exports.APP_VERSION = exports.get
10
10
  * ERROR CODES:
11
11
  * 594: waitforsignal
12
12
  * 592: sleepFor
13
- * 595: sleep (deprecated)
14
13
  * 596, 597, 598: fatal
15
14
  * 599: retry
16
15
  */
@@ -125,19 +124,6 @@ const getWorkflowYAML = (app, version) => {
125
124
  maps:
126
125
  index: '{$self.output.data.index}'
127
126
  signals: '{$self.output.data.signals}'
128
- 595:
129
- schema:
130
- type: object
131
- properties:
132
- duration:
133
- type: number
134
- description: sleep duration in seconds
135
- index:
136
- type: number
137
- description: the current index
138
- maps:
139
- duration: '{$self.output.data.duration}'
140
- index: '{$self.output.data.index}'
141
127
  592:
142
128
  schema:
143
129
  type: object
@@ -232,19 +218,6 @@ const getWorkflowYAML = (app, version) => {
232
218
  maps:
233
219
  index: '{$self.output.data.index}'
234
220
  signals: '{$self.output.data.signals}'
235
- 595:
236
- schema:
237
- type: object
238
- properties:
239
- duration:
240
- type: number
241
- description: sleep duration in seconds
242
- index:
243
- type: number
244
- description: the current index
245
- maps:
246
- duration: '{$self.output.data.duration}'
247
- index: '{$self.output.data.index}'
248
221
  592:
249
222
  schema:
250
223
  type: object
@@ -323,57 +296,6 @@ const getWorkflowYAML = (app, version) => {
323
296
  maps:
324
297
  duration: '{siga1.output.data.duration}'
325
298
 
326
- siga595:
327
- title: Signal In - Sleep before trying again
328
- type: await
329
- topic: ${app}.sleep.execute
330
- input:
331
- schema:
332
- type: object
333
- properties:
334
- duration:
335
- type: number
336
- index:
337
- type: number
338
- workflowId:
339
- type: string
340
- parentWorkflowId:
341
- type: string
342
- originJobId:
343
- type: string
344
- maps:
345
- duration: '{sigw1.output.data.duration}'
346
- index: '{sigw1.output.data.index}'
347
- parentWorkflowId:
348
- '@pipe':
349
- - ['{$job.metadata.jid}', '-s']
350
- - ['{@string.concat}']
351
- originJobId:
352
- '@pipe':
353
- - ['{t1.output.data.originJobId}', '{t1.output.data.originJobId}', '{$job.metadata.jid}']
354
- - ['{@conditional.ternary}']
355
-
356
- workflowId:
357
- '@pipe':
358
- - ['-', '{$job.metadata.jid}', '-$sleep', '{sig.output.metadata.dad}', '-', '{sigw1.output.data.index}']
359
- - ['{@string.concat}']
360
- output:
361
- schema:
362
- type: object
363
- properties:
364
- done:
365
- type: boolean
366
- maps:
367
- done: '{sigw1.output.data.done}'
368
-
369
- sigc595:
370
- title: Signal In - Goto Activity siga1
371
- type: cycle
372
- ancestor: siga1
373
- input:
374
- maps:
375
- duration: '{siga1.output.data.duration}'
376
-
377
299
  siga592:
378
300
  title: Signal In - Sleep For a duration and then cycle/goto
379
301
  type: hook
@@ -467,56 +389,6 @@ const getWorkflowYAML = (app, version) => {
467
389
  maps:
468
390
  duration: '{a1.output.data.duration}'
469
391
 
470
- a595:
471
- title: Sleep before trying again
472
- type: await
473
- topic: ${app}.sleep.execute
474
- input:
475
- schema:
476
- type: object
477
- properties:
478
- duration:
479
- type: number
480
- index:
481
- type: number
482
- workflowId:
483
- type: string
484
- parentWorkflowId:
485
- type: string
486
- originJobId:
487
- type: string
488
- maps:
489
- duration: '{w1.output.data.duration}'
490
- index: '{w1.output.data.index}'
491
- parentWorkflowId:
492
- '@pipe':
493
- - ['{$job.metadata.jid}', '-s']
494
- - ['{@string.concat}']
495
- originJobId:
496
- '@pipe':
497
- - ['{t1.output.data.originJobId}', '{t1.output.data.originJobId}', '{$job.metadata.jid}']
498
- - ['{@conditional.ternary}']
499
- workflowId:
500
- '@pipe':
501
- - ['-', '{$job.metadata.jid}', '-$sleep-', '{w1.output.data.index}']
502
- - ['{@string.concat}']
503
- output:
504
- schema:
505
- type: object
506
- properties:
507
- done:
508
- type: boolean
509
- maps:
510
- done: '{w1.output.data.done}'
511
-
512
- c595:
513
- title: Goto Activity a1
514
- type: cycle
515
- ancestor: a1
516
- input:
517
- maps:
518
- duration: '{a1.output.data.duration}'
519
-
520
392
  a592:
521
393
  title: Sleep For a duration and then cycle/goto
522
394
  type: hook
@@ -574,9 +446,6 @@ const getWorkflowYAML = (app, version) => {
574
446
  - to: siga594
575
447
  conditions:
576
448
  code: 594
577
- - to: siga595
578
- conditions:
579
- code: 595
580
449
  - to: siga592
581
450
  conditions:
582
451
  code: 592
@@ -585,8 +454,6 @@ const getWorkflowYAML = (app, version) => {
585
454
  code: 599
586
455
  siga594:
587
456
  - to: sigc594
588
- siga595:
589
- - to: sigc595
590
457
  siga592:
591
458
  - to: sigc592
592
459
  siga599:
@@ -597,9 +464,6 @@ const getWorkflowYAML = (app, version) => {
597
464
  - to: a594
598
465
  conditions:
599
466
  code: 594
600
- - to: a595
601
- conditions:
602
- code: 595
603
467
  - to: a592
604
468
  conditions:
605
469
  code: 592
@@ -611,8 +475,6 @@ const getWorkflowYAML = (app, version) => {
611
475
  code: [200, 598, 597, 596]
612
476
  a594:
613
477
  - to: c594
614
- a595:
615
- - to: c595
616
478
  a592:
617
479
  - to: c592
618
480
  a599:
@@ -187,21 +187,6 @@ class WorkerService {
187
187
  catch (err) {
188
188
  //not an error...just a trigger to sleep
189
189
  if (err instanceof errors_1.DurableSleepForError) {
190
- return {
191
- status: stream_1.StreamStatus.SUCCESS,
192
- code: err.code,
193
- metadata: { ...data.metadata },
194
- data: {
195
- code: err.code,
196
- message: JSON.stringify({ duration: err.duration, index: err.index, dimension: err.dimension }),
197
- duration: err.duration,
198
- index: err.index,
199
- dimension: err.dimension
200
- }
201
- };
202
- //deprecated format; not an error...just a trigger to sleep
203
- }
204
- else if (err instanceof errors_1.DurableSleepError) {
205
190
  return {
206
191
  status: stream_1.StreamStatus.SUCCESS,
207
192
  code: err.code,
@@ -112,15 +112,6 @@ export declare class WorkflowService {
112
112
  * @returns {Promise<number>}
113
113
  */
114
114
  static sleepFor(duration: string): Promise<number>;
115
- /**
116
- * Sleeps the workflow for a duration. As the function is reentrant,
117
- * upon reentry, the function will traverse prior execution paths up
118
- * until the sleep command and then resume execution from that point.
119
- * @param {string} duration - for example: '1 minute', '2 hours', '3 days'
120
- * @returns {Promise<number>}
121
- * @deprecated - use `sleepFor` instead
122
- */
123
- static sleep(duration: string): Promise<number>;
124
115
  /**
125
116
  * Waits for a signal to awaken
126
117
  * @param {string[]} signals - the signals to wait for
@@ -338,35 +338,6 @@ class WorkflowService {
338
338
  }
339
339
  return seconds;
340
340
  }
341
- /**
342
- * Sleeps the workflow for a duration. As the function is reentrant,
343
- * upon reentry, the function will traverse prior execution paths up
344
- * until the sleep command and then resume execution from that point.
345
- * @param {string} duration - for example: '1 minute', '2 hours', '3 days'
346
- * @returns {Promise<number>}
347
- * @deprecated - use `sleepFor` instead
348
- */
349
- static async sleep(duration) {
350
- const seconds = (0, ms_1.default)(duration) / 1000;
351
- const store = storage_1.asyncLocalStorage.getStore();
352
- const COUNTER = store.get('counter');
353
- const execIndex = COUNTER.counter = COUNTER.counter + 1;
354
- const workflowId = store.get('workflowId');
355
- const workflowTopic = store.get('workflowTopic');
356
- const workflowDimension = store.get('workflowDimension') ?? '';
357
- const namespace = store.get('namespace');
358
- const sleepJobId = `-${workflowId}-$sleep${workflowDimension}-${execIndex}`;
359
- try {
360
- const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
361
- await hotMeshClient.getState(`${hotMeshClient.appId}.sleep.execute`, sleepJobId);
362
- //if no error is thrown, we've already slept, return the delay
363
- return seconds;
364
- }
365
- catch (e) {
366
- // spawn a new sleep job if error code 595 is thrown by the worker)
367
- throw new errors_1.DurableSleepError(workflowId, seconds, execIndex, workflowDimension);
368
- }
369
- }
370
341
  /**
371
342
  * Waits for a signal to awaken
372
343
  * @param {string[]} signals - the signals to wait for
@@ -66,7 +66,7 @@ declare class EngineService {
66
66
  interrupt(topic: string, jobId: string, options?: JobInterruptOptions): Promise<string>;
67
67
  scrub(jobId: string): Promise<void>;
68
68
  hook(topic: string, data: JobData, status?: StreamStatus, code?: StreamCode): Promise<string>;
69
- hookTime(jobId: string, gId: string, activityId: string, type?: WorkListTaskType): Promise<string | void>;
69
+ hookTime(jobId: string, gId: string, topicOrActivity: string, type?: WorkListTaskType): Promise<string | void>;
70
70
  hookAll(hookTopic: string, data: JobData, keyResolver: JobStatsInput, queryFacets?: string[]): Promise<string[]>;
71
71
  pub(topic: string, data: JobData, context?: JobState): Promise<string>;
72
72
  sub(topic: string, callback: JobMessageCallback): Promise<void>;
@@ -226,6 +226,7 @@ class EngineService {
226
226
  });
227
227
  const context = {
228
228
  metadata: {
229
+ guid: streamData.metadata.guid,
229
230
  jid: streamData.metadata.jid,
230
231
  gid: streamData.metadata.gid,
231
232
  dad: streamData.metadata.dad,
@@ -353,15 +354,11 @@ class EngineService {
353
354
  };
354
355
  return await this.router.publishMessage(null, streamData);
355
356
  }
356
- async hookTime(jobId, gId, activityId, type) {
357
- if (type === 'interrupt') {
358
- return await this.interrupt(activityId, //note: 'activityId' is the actually job topic
359
- jobId, { suppress: true, expire: 1 });
357
+ async hookTime(jobId, gId, topicOrActivity, type) {
358
+ if (type === 'interrupt' || type === 'expire') {
359
+ return await this.interrupt(topicOrActivity, jobId, { suppress: true, expire: 1 });
360
360
  }
361
- else if (type === 'expire') {
362
- return await this.store.expireJob(jobId, 1);
363
- }
364
- const [aid, ...dimensions] = activityId.split(',');
361
+ const [aid, ...dimensions] = topicOrActivity.split(',');
365
362
  const dad = `,${dimensions.join(',')}`;
366
363
  const streamData = {
367
364
  type: stream_1.StreamDataType.TIMEHOOK,
@@ -86,7 +86,18 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
86
86
  */
87
87
  getQueryState(jobId: string, fields: string[]): Promise<StringAnyType>;
88
88
  getState(jobId: string, consumes: Consumes, dIds: StringStringType): Promise<[StringAnyType, number] | undefined>;
89
+ /**
90
+ * collate is a generic method for incrementing a value in a hash
91
+ * in order to track their progress during processing.
92
+ */
89
93
  collate(jobId: string, activityId: string, amount: number, dIds: StringStringType, multi?: U): Promise<number>;
94
+ /**
95
+ * synthentic collation affects those activities in the graph
96
+ * that represent the synthetic DAG that was materialized during compilation;
97
+ * Synthetic targeting ensures that re-entry due to failure can be distinguished from
98
+ * purposeful re-entry.
99
+ */
100
+ collateSynthetic(jobId: string, guid: string, amount: number, multi?: U): Promise<number>;
90
101
  setStateNX(jobId: string, appId: string): Promise<boolean>;
91
102
  getSchema(activityId: string, appVersion: AppVID): Promise<ActivityType>;
92
103
  getSchemas(appVersion: AppVID): Promise<Record<string, ActivityType>>;
@@ -485,6 +485,10 @@ class StoreService {
485
485
  throw new errors_1.GetStateError(jobId);
486
486
  }
487
487
  }
488
+ /**
489
+ * collate is a generic method for incrementing a value in a hash
490
+ * in order to track their progress during processing.
491
+ */
488
492
  async collate(jobId, activityId, amount, dIds, multi) {
489
493
  const jobKey = this.mintKey(key_1.KeyType.JOB_STATE, { appId: this.appId, jobId });
490
494
  const collationKey = `${activityId}/output/metadata/as`; //activity state
@@ -497,6 +501,16 @@ class StoreService {
497
501
  const targetId = Object.keys(hashData)[0];
498
502
  return await (multi || this.redisClient)[this.commands.hincrbyfloat](jobKey, targetId, amount);
499
503
  }
504
+ /**
505
+ * synthentic collation affects those activities in the graph
506
+ * that represent the synthetic DAG that was materialized during compilation;
507
+ * Synthetic targeting ensures that re-entry due to failure can be distinguished from
508
+ * purposeful re-entry.
509
+ */
510
+ async collateSynthetic(jobId, guid, amount, multi) {
511
+ const jobKey = this.mintKey(key_1.KeyType.JOB_STATE, { appId: this.appId, jobId });
512
+ return await (multi || this.redisClient)[this.commands.hincrbyfloat](jobKey, guid, amount);
513
+ }
500
514
  async setStateNX(jobId, appId) {
501
515
  const hashKey = this.mintKey(key_1.KeyType.JOB_STATE, { appId, jobId });
502
516
  const result = await this.redisClient[this.commands.hsetnx](hashKey, ':', '1');
@@ -6,6 +6,7 @@ type ActivityData = {
6
6
  };
7
7
  type JobMetadata = {
8
8
  key?: string;
9
+ guid?: string;
9
10
  gid: string;
10
11
  jid: string;
11
12
  dad: string;
package/modules/errors.ts CHANGED
@@ -45,20 +45,6 @@ class DurableWaitForSignalError extends Error {
45
45
  }
46
46
  }
47
47
 
48
- /* @deprecated */
49
- class DurableSleepError extends Error {
50
- code: number;
51
- duration: number; //seconds
52
- index: number; //execution order in the workflow
53
- dimension: string; //hook dimension (e.g., ',0,1,0') (uses empty string for `null`)
54
- constructor(message: string, duration: number, index: number, dimension: string) {
55
- super(message);
56
- this.duration = duration;
57
- this.index = index;
58
- this.dimension = dimension;
59
- this.code = 595;
60
- }
61
- }
62
48
  class DurableSleepForError extends Error {
63
49
  code: number;
64
50
  duration: number; //seconds
@@ -175,7 +161,6 @@ export {
175
161
  DurableIncompleteSignalError,
176
162
  DurableMaxedError,
177
163
  DurableRetryError,
178
- DurableSleepError,
179
164
  DurableSleepForError,
180
165
  DurableTimeoutError,
181
166
  DurableWaitForSignalError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.41",
3
+ "version": "0.0.42",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -90,6 +90,22 @@ class Activity {
90
90
  await CollatorService.notarizeEntry(this);
91
91
  }
92
92
 
93
+ /**
94
+ * Upon entering leg 2 of a duplexed activty, verify
95
+ * all aspects of the re-entry including job and activty state
96
+ */
97
+ async verifyReentry(): Promise<number> {
98
+ const guid = this.context.metadata.guid;
99
+ this.setLeg(2);
100
+ await this.getState();
101
+ CollatorService.assertJobActive(
102
+ this.context.metadata.js,
103
+ this.context.metadata.jid,
104
+ this.metadata.aid
105
+ );
106
+ return await CollatorService.notarizeReentry(this, guid);
107
+ }
108
+
93
109
  //******** DUPLEX RE-ENTRY POINT ********//
94
110
  async processEvent(status: StreamStatus = StreamStatus.SUCCESS, code: StreamCode = 200, type: 'hook' | 'output' = 'output'): Promise<void> {
95
111
  this.setLeg(2);
@@ -103,23 +119,20 @@ class Activity {
103
119
  this.code = code;
104
120
  this.logger.debug('activity-process-event', { topic: this.config.subtype, jid, aid, status, code });
105
121
  let telemetry: TelemetryService;
106
- try {
107
- await this.getState();
108
- CollatorService.assertJobActive(this.context.metadata.js, this.context.metadata.jid, this.metadata.aid);
109
- const aState = await CollatorService.notarizeReentry(this);
110
- this.adjacentIndex = CollatorService.getDimensionalIndex(aState);
111
-
112
- telemetry = new TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
113
- let isComplete = CollatorService.isActivityComplete(this.context.metadata.js);
114
122
 
115
- if (isComplete) {
116
- this.logger.warn('activity-process-event-duplicate', { jid, aid });
117
- this.logger.debug('activity-process-event-duplicate-resolution', { resolution: 'Increase HotMesh config `reclaimDelay` timeout.' });
118
- return;
119
- }
123
+ try {
124
+ const collationKey = await this.verifyReentry();
120
125
 
126
+ this.adjacentIndex = CollatorService.getDimensionalIndex(collationKey);
127
+ telemetry = new TelemetryService(
128
+ this.engine.appId,
129
+ this.config,
130
+ this.metadata,
131
+ this.context,
132
+ );
121
133
  telemetry.startActivitySpan(this.leg);
122
134
  let multiResponse: MultiResponseFlags;
135
+
123
136
  if (status === StreamStatus.PENDING) {
124
137
  multiResponse = await this.processPending(telemetry, type);
125
138
  } else if (status === StreamStatus.SUCCESS) {
@@ -384,7 +397,7 @@ class Activity {
384
397
  TelemetryService.addTargetTelemetryPaths(consumes, this.config, this.metadata, this.leg);
385
398
  let { dad, jid } = this.context.metadata;
386
399
  const dIds = CollatorService.getDimensionsById([...this.config.ancestors, this.metadata.aid], dad || '');
387
- //`state` is a flat hash; context is a tree
400
+ //`state` is a unidimensional hash; context is a tree
388
401
  const [state, status] = await this.store.getState(jid, consumes, dIds);
389
402
  this.context = restoreHierarchy(state) as JobState;
390
403
  this.assertGenerationalId(this.context.metadata.gid, gid);
@@ -395,7 +408,9 @@ class Activity {
395
408
 
396
409
  /**
397
410
  * if the job is created/deleted/created with the same key,
398
- * the 'gid' ensures no stale messages enter the stream
411
+ * the 'gid' ensures no stale messages (such as sleep delays)
412
+ * enter the workstream. Any message with a mismatched gid
413
+ * belongs to a prior job and can safely be ignored/dropped.
399
414
  */
400
415
  assertGenerationalId(jobGID: string, msgGID?: string) {
401
416
  if (msgGID !== jobGID) {
@@ -78,11 +78,28 @@ class CollatorService {
78
78
  return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1_000_001 - decrement, this.getDimensionalAddress(activity), multi);
79
79
  };
80
80
 
81
- static async notarizeReentry(activity: Activity, multi?: RedisMulti): Promise<number> {
81
+ /**
82
+ * verifies both the concrete and synthetic keys for the activity; concrete keys
83
+ * exist in the original model and are effectively the 'real' keys. In reality,
84
+ * hook activities are atomized during compilation to create a synthetic DAG that
85
+ * is used to track the status of the graph in a distributed environment. The
86
+ * synthetic key represents different dimensional realities and is used to
87
+ * track re-entry overages (it distinguishes between the original and re-entry).
88
+ * The essential challenge is: is this a re-entry that is purposeful in
89
+ * order to induce cycles, or is the re-entry due to a failure in the system?
90
+ */
91
+ static async notarizeReentry(activity: Activity, guid: string, multi?: RedisMulti): Promise<number> {
92
+ const jid = activity.context.metadata.jid;
93
+ const localMulti = multi || activity.store.getMulti();
82
94
  //increment by 1_000_000 (indicates re-entry and is used to drive the 'dimensional address' for adjacent activities (minus 1))
83
- const amount = await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1_000_000, this.getDimensionalAddress(activity, true), multi);
84
- this.verifyInteger(amount, 2, 'enter');
85
- return amount;
95
+ await activity.store.collate(jid, activity.metadata.aid, 1_000_000, this.getDimensionalAddress(activity, true), localMulti);
96
+ await activity.store.collateSynthetic(jid, guid, 1_000_000, localMulti);
97
+ const [_amountConcrete, _amountSynthetic] = await localMulti.exec();
98
+ const amountConcrete = Array.isArray(_amountConcrete) ? _amountConcrete[1] : _amountConcrete;
99
+ const amountSynthetic = Array.isArray(_amountSynthetic) ? _amountSynthetic[1] : _amountSynthetic;
100
+ this.verifyInteger(amountConcrete as number, 2, 'enter');
101
+ this.verifySyntheticInteger(amountSynthetic as number);
102
+ return amountConcrete as number;
86
103
  };
87
104
 
88
105
  static async notarizeContinuation(activity: Activity, multi?: RedisMulti): Promise<number> {
@@ -134,6 +151,26 @@ class CollatorService {
134
151
  }
135
152
  }
136
153
 
154
+ /**
155
+ * During compilation, the graphs are compiled into structures necessary
156
+ * for distributed processing; these are referred to as 'synthetic DAGs',
157
+ * because they are not part of the original graph, but are used to track
158
+ * the status of the graph in a distributed environment. This check ensures
159
+ * that the 'synthetic key' is not a duplicate. (which is different than
160
+ * saying the 'key' is not a duplicate)
161
+ */
162
+ static verifySyntheticInteger(amount: number): void {
163
+ const samount = amount.toString();
164
+ const isCompletedValue = parseInt(samount[samount.length - 1], 10);
165
+ if (isCompletedValue > 0) {
166
+ //already done error (ack/delete clearly failed; this is a duplicate)
167
+ throw new CollationError(amount, 2, 'enter', CollationFaultType.INACTIVE);
168
+ } else if (amount >= 2_000_000) {
169
+ //duplicate synthetic key (todo: need to resolve/fix this!!)
170
+ throw new CollationError(amount, 2, 'enter', CollationFaultType.DUPLICATE);
171
+ }
172
+ }
173
+
137
174
  static verifyInteger(amount: number, leg: ActivityDuplex, stage: CollationStage): void {
138
175
  let faultType: CollationFaultType | undefined;
139
176
  if (leg === 1 && stage === 'enter') {
@@ -239,10 +276,6 @@ class CollatorService {
239
276
  });
240
277
  }
241
278
 
242
- static isActivityComplete(status: number): boolean {
243
- return (status - 0) <= 0;
244
- }
245
-
246
279
  /**
247
280
  * All activities exist on a dimensional plane. Zero
248
281
  * is the default. A value of
@@ -7,7 +7,6 @@
7
7
  * ERROR CODES:
8
8
  * 594: waitforsignal
9
9
  * 592: sleepFor
10
- * 595: sleep (deprecated)
11
10
  * 596, 597, 598: fatal
12
11
  * 599: retry
13
12
  */
@@ -122,19 +121,6 @@ const getWorkflowYAML = (app: string, version: string) => {
122
121
  maps:
123
122
  index: '{$self.output.data.index}'
124
123
  signals: '{$self.output.data.signals}'
125
- 595:
126
- schema:
127
- type: object
128
- properties:
129
- duration:
130
- type: number
131
- description: sleep duration in seconds
132
- index:
133
- type: number
134
- description: the current index
135
- maps:
136
- duration: '{$self.output.data.duration}'
137
- index: '{$self.output.data.index}'
138
124
  592:
139
125
  schema:
140
126
  type: object
@@ -229,19 +215,6 @@ const getWorkflowYAML = (app: string, version: string) => {
229
215
  maps:
230
216
  index: '{$self.output.data.index}'
231
217
  signals: '{$self.output.data.signals}'
232
- 595:
233
- schema:
234
- type: object
235
- properties:
236
- duration:
237
- type: number
238
- description: sleep duration in seconds
239
- index:
240
- type: number
241
- description: the current index
242
- maps:
243
- duration: '{$self.output.data.duration}'
244
- index: '{$self.output.data.index}'
245
218
  592:
246
219
  schema:
247
220
  type: object
@@ -320,57 +293,6 @@ const getWorkflowYAML = (app: string, version: string) => {
320
293
  maps:
321
294
  duration: '{siga1.output.data.duration}'
322
295
 
323
- siga595:
324
- title: Signal In - Sleep before trying again
325
- type: await
326
- topic: ${app}.sleep.execute
327
- input:
328
- schema:
329
- type: object
330
- properties:
331
- duration:
332
- type: number
333
- index:
334
- type: number
335
- workflowId:
336
- type: string
337
- parentWorkflowId:
338
- type: string
339
- originJobId:
340
- type: string
341
- maps:
342
- duration: '{sigw1.output.data.duration}'
343
- index: '{sigw1.output.data.index}'
344
- parentWorkflowId:
345
- '@pipe':
346
- - ['{$job.metadata.jid}', '-s']
347
- - ['{@string.concat}']
348
- originJobId:
349
- '@pipe':
350
- - ['{t1.output.data.originJobId}', '{t1.output.data.originJobId}', '{$job.metadata.jid}']
351
- - ['{@conditional.ternary}']
352
-
353
- workflowId:
354
- '@pipe':
355
- - ['-', '{$job.metadata.jid}', '-$sleep', '{sig.output.metadata.dad}', '-', '{sigw1.output.data.index}']
356
- - ['{@string.concat}']
357
- output:
358
- schema:
359
- type: object
360
- properties:
361
- done:
362
- type: boolean
363
- maps:
364
- done: '{sigw1.output.data.done}'
365
-
366
- sigc595:
367
- title: Signal In - Goto Activity siga1
368
- type: cycle
369
- ancestor: siga1
370
- input:
371
- maps:
372
- duration: '{siga1.output.data.duration}'
373
-
374
296
  siga592:
375
297
  title: Signal In - Sleep For a duration and then cycle/goto
376
298
  type: hook
@@ -464,56 +386,6 @@ const getWorkflowYAML = (app: string, version: string) => {
464
386
  maps:
465
387
  duration: '{a1.output.data.duration}'
466
388
 
467
- a595:
468
- title: Sleep before trying again
469
- type: await
470
- topic: ${app}.sleep.execute
471
- input:
472
- schema:
473
- type: object
474
- properties:
475
- duration:
476
- type: number
477
- index:
478
- type: number
479
- workflowId:
480
- type: string
481
- parentWorkflowId:
482
- type: string
483
- originJobId:
484
- type: string
485
- maps:
486
- duration: '{w1.output.data.duration}'
487
- index: '{w1.output.data.index}'
488
- parentWorkflowId:
489
- '@pipe':
490
- - ['{$job.metadata.jid}', '-s']
491
- - ['{@string.concat}']
492
- originJobId:
493
- '@pipe':
494
- - ['{t1.output.data.originJobId}', '{t1.output.data.originJobId}', '{$job.metadata.jid}']
495
- - ['{@conditional.ternary}']
496
- workflowId:
497
- '@pipe':
498
- - ['-', '{$job.metadata.jid}', '-$sleep-', '{w1.output.data.index}']
499
- - ['{@string.concat}']
500
- output:
501
- schema:
502
- type: object
503
- properties:
504
- done:
505
- type: boolean
506
- maps:
507
- done: '{w1.output.data.done}'
508
-
509
- c595:
510
- title: Goto Activity a1
511
- type: cycle
512
- ancestor: a1
513
- input:
514
- maps:
515
- duration: '{a1.output.data.duration}'
516
-
517
389
  a592:
518
390
  title: Sleep For a duration and then cycle/goto
519
391
  type: hook
@@ -571,9 +443,6 @@ const getWorkflowYAML = (app: string, version: string) => {
571
443
  - to: siga594
572
444
  conditions:
573
445
  code: 594
574
- - to: siga595
575
- conditions:
576
- code: 595
577
446
  - to: siga592
578
447
  conditions:
579
448
  code: 592
@@ -582,8 +451,6 @@ const getWorkflowYAML = (app: string, version: string) => {
582
451
  code: 599
583
452
  siga594:
584
453
  - to: sigc594
585
- siga595:
586
- - to: sigc595
587
454
  siga592:
588
455
  - to: sigc592
589
456
  siga599:
@@ -594,9 +461,6 @@ const getWorkflowYAML = (app: string, version: string) => {
594
461
  - to: a594
595
462
  conditions:
596
463
  code: 594
597
- - to: a595
598
- conditions:
599
- code: 595
600
464
  - to: a592
601
465
  conditions:
602
466
  code: 592
@@ -608,8 +472,6 @@ const getWorkflowYAML = (app: string, version: string) => {
608
472
  code: [200, 598, 597, 596]
609
473
  a594:
610
474
  - to: c594
611
- a595:
612
- - to: c595
613
475
  a592:
614
476
  - to: c592
615
477
  a599:
@@ -3,7 +3,6 @@ import {
3
3
  DurableIncompleteSignalError,
4
4
  DurableMaxedError,
5
5
  DurableRetryError,
6
- DurableSleepError,
7
6
  DurableSleepForError,
8
7
  DurableTimeoutError,
9
8
  DurableWaitForSignalError} from '../../modules/errors';
@@ -249,21 +248,6 @@ export class WorkerService {
249
248
  }
250
249
  } as StreamDataResponse;
251
250
 
252
- //deprecated format; not an error...just a trigger to sleep
253
- } else if (err instanceof DurableSleepError) {
254
- return {
255
- status: StreamStatus.SUCCESS,
256
- code: err.code,
257
- metadata: { ...data.metadata },
258
- data: {
259
- code: err.code,
260
- message: JSON.stringify({ duration: err.duration, index: err.index, dimension: err.dimension }),
261
- duration: err.duration,
262
- index: err.index,
263
- dimension: err.dimension
264
- }
265
- } as StreamDataResponse;
266
-
267
251
  //not an error...just a trigger to wait for a signal
268
252
  } else if (err instanceof DurableWaitForSignalError) {
269
253
  return {
@@ -2,7 +2,6 @@ import ms from 'ms';
2
2
 
3
3
  import {
4
4
  DurableIncompleteSignalError,
5
- DurableSleepError,
6
5
  DurableSleepForError,
7
6
  DurableWaitForSignalError } from '../../modules/errors';
8
7
  import { KeyService, KeyType } from '../../modules/key';
@@ -376,37 +375,6 @@ export class WorkflowService {
376
375
  return seconds;
377
376
  }
378
377
 
379
- /**
380
- * Sleeps the workflow for a duration. As the function is reentrant,
381
- * upon reentry, the function will traverse prior execution paths up
382
- * until the sleep command and then resume execution from that point.
383
- * @param {string} duration - for example: '1 minute', '2 hours', '3 days'
384
- * @returns {Promise<number>}
385
- * @deprecated - use `sleepFor` instead
386
- */
387
- static async sleep(duration: string): Promise<number> {
388
- const seconds = ms(duration) / 1000;
389
-
390
- const store = asyncLocalStorage.getStore();
391
- const COUNTER = store.get('counter');
392
- const execIndex = COUNTER.counter = COUNTER.counter + 1;
393
- const workflowId = store.get('workflowId');
394
- const workflowTopic = store.get('workflowTopic');
395
- const workflowDimension = store.get('workflowDimension') ?? '';
396
- const namespace = store.get('namespace');
397
- const sleepJobId = `-${workflowId}-$sleep${workflowDimension}-${execIndex}`;
398
-
399
- try {
400
- const hotMeshClient = await WorkerService.getHotMesh(workflowTopic, { namespace });
401
- await hotMeshClient.getState(`${hotMeshClient.appId}.sleep.execute`, sleepJobId);
402
- //if no error is thrown, we've already slept, return the delay
403
- return seconds;
404
- } catch (e) {
405
- // spawn a new sleep job if error code 595 is thrown by the worker)
406
- throw new DurableSleepError(workflowId, seconds, execIndex, workflowDimension);
407
- }
408
- }
409
-
410
378
  /**
411
379
  * Waits for a signal to awaken
412
380
  * @param {string[]} signals - the signals to wait for
@@ -332,6 +332,7 @@ class EngineService {
332
332
  });
333
333
  const context: PartialJobState = {
334
334
  metadata: {
335
+ guid: streamData.metadata.guid,
335
336
  jid: streamData.metadata.jid,
336
337
  gid: streamData.metadata.gid,
337
338
  dad: streamData.metadata.dad,
@@ -490,17 +491,15 @@ class EngineService {
490
491
  };
491
492
  return await this.router.publishMessage(null, streamData) as string;
492
493
  }
493
- async hookTime(jobId: string, gId: string, activityId: string, type?: WorkListTaskType): Promise<string | void> {
494
- if (type === 'interrupt') {
494
+ async hookTime(jobId: string, gId: string, topicOrActivity: string, type?: WorkListTaskType): Promise<string | void> {
495
+ if (type === 'interrupt' || type === 'expire') {
495
496
  return await this.interrupt(
496
- activityId, //note: 'activityId' is the actually job topic
497
+ topicOrActivity,
497
498
  jobId,
498
499
  { suppress: true, expire: 1 },
499
500
  );
500
- } else if (type === 'expire') {
501
- return await this.store.expireJob(jobId, 1);
502
501
  }
503
- const [aid, ...dimensions] = activityId.split(',');
502
+ const [aid, ...dimensions] = topicOrActivity.split(',');
504
503
  const dad = `,${dimensions.join(',')}`;
505
504
  const streamData: StreamData = {
506
505
  type: StreamDataType.TIMEHOOK,
@@ -586,6 +586,10 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
586
586
  }
587
587
  }
588
588
 
589
+ /**
590
+ * collate is a generic method for incrementing a value in a hash
591
+ * in order to track their progress during processing.
592
+ */
589
593
  async collate(jobId: string, activityId: string, amount: number, dIds: StringStringType, multi? : U): Promise<number> {
590
594
  const jobKey = this.mintKey(KeyType.JOB_STATE, { appId: this.appId, jobId });
591
595
  const collationKey = `${activityId}/output/metadata/as`; //activity state
@@ -600,6 +604,17 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
600
604
  return await (multi || this.redisClient)[this.commands.hincrbyfloat](jobKey, targetId, amount);
601
605
  }
602
606
 
607
+ /**
608
+ * synthentic collation affects those activities in the graph
609
+ * that represent the synthetic DAG that was materialized during compilation;
610
+ * Synthetic targeting ensures that re-entry due to failure can be distinguished from
611
+ * purposeful re-entry.
612
+ */
613
+ async collateSynthetic(jobId: string, guid: string, amount: number, multi? : U): Promise<number> {
614
+ const jobKey = this.mintKey(KeyType.JOB_STATE, { appId: this.appId, jobId });
615
+ return await (multi || this.redisClient)[this.commands.hincrbyfloat](jobKey, guid, amount);
616
+ }
617
+
603
618
  async setStateNX(jobId: string, appId: string): Promise<boolean> {
604
619
  const hashKey = this.mintKey(KeyType.JOB_STATE, { appId, jobId });
605
620
  const result = await this.redisClient[this.commands.hsetnx](hashKey, ':', '1');
package/types/job.ts CHANGED
@@ -8,6 +8,7 @@ type ActivityData = {
8
8
 
9
9
  type JobMetadata = {
10
10
  key?: string; //job_key
11
+ guid?: string; //system assigned guid that corresponds to the transition message guid that spawned reentry
11
12
  gid: string; //system assigned guid; ensured created/deleted/created jobs are unique
12
13
  jid: string; //job_id (jid+dad+aid) is composite key for activity
13
14
  dad: string; //dimensional address for the activity (,0,0,1)