@hotmeshio/hotmesh 0.0.34 → 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 (91) 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/index.d.ts +40 -4
  44. package/build/services/store/index.js +114 -9
  45. package/build/services/task/index.d.ts +5 -4
  46. package/build/services/task/index.js +12 -14
  47. package/build/types/activity.d.ts +35 -5
  48. package/build/types/durable.d.ts +4 -0
  49. package/build/types/index.d.ts +1 -1
  50. package/build/types/job.d.ts +18 -1
  51. package/build/types/quorum.d.ts +11 -7
  52. package/build/types/stream.d.ts +4 -1
  53. package/build/types/stream.js +2 -0
  54. package/modules/enums.ts +32 -0
  55. package/modules/errors.ts +24 -9
  56. package/modules/key.ts +4 -1
  57. package/modules/utils.ts +5 -0
  58. package/package.json +3 -1
  59. package/services/activities/activity.ts +34 -8
  60. package/services/activities/await.ts +11 -4
  61. package/services/activities/cycle.ts +10 -3
  62. package/services/activities/hook.ts +8 -3
  63. package/services/activities/index.ts +2 -2
  64. package/services/activities/interrupt.ts +159 -0
  65. package/services/activities/signal.ts +9 -3
  66. package/services/activities/trigger.ts +21 -5
  67. package/services/activities/worker.ts +10 -3
  68. package/services/collator/index.ts +10 -1
  69. package/services/compiler/deployer.ts +1 -3
  70. package/services/connector/index.ts +3 -5
  71. package/services/durable/client.ts +8 -4
  72. package/services/durable/factory.ts +65 -284
  73. package/services/durable/handle.ts +55 -9
  74. package/services/durable/meshos.ts +2 -3
  75. package/services/durable/worker.ts +9 -2
  76. package/services/durable/workflow.ts +66 -2
  77. package/services/engine/index.ts +74 -26
  78. package/services/hotmesh/index.ts +14 -4
  79. package/services/quorum/index.ts +9 -0
  80. package/services/signaler/stream.ts +27 -24
  81. package/services/store/index.ts +119 -11
  82. package/services/task/index.ts +18 -18
  83. package/types/activity.ts +38 -8
  84. package/types/durable.ts +8 -4
  85. package/types/index.ts +1 -1
  86. package/types/job.ts +30 -1
  87. package/types/quorum.ts +13 -8
  88. package/types/stream.ts +3 -0
  89. package/build/services/activities/iterate.d.ts +0 -9
  90. package/build/services/activities/iterate.js +0 -13
  91. package/services/activities/iterate.ts +0 -26
@@ -1,5 +1,16 @@
1
+ import {
2
+ BLOCK_TIME_MS,
3
+ MAX_RETRIES,
4
+ MAX_TIMEOUT_MS,
5
+ GRADUATED_INTERVAL_MS,
6
+ STATUS_CODE_UNACKED,
7
+ STATUS_CODE_UNKNOWN,
8
+ STATUS_MESSAGE_UNKNOWN,
9
+ XCLAIM_COUNT,
10
+ XCLAIM_DELAY_MS,
11
+ XPENDING_COUNT } from '../../modules/enums';
1
12
  import { KeyType } from '../../modules/key';
2
- import { XSleepFor, sleepFor } from '../../modules/utils';
13
+ import { XSleepFor, guid, sleepFor } from '../../modules/utils';
3
14
  import { ILogger } from '../logger';
4
15
  import { StoreService } from '../store';
5
16
  import { StreamService } from '../stream';
@@ -16,19 +27,6 @@ import {
16
27
  StreamStatus
17
28
  } from '../../types/stream';
18
29
 
19
- const MAX_RETRIES = 3; //local retry; 10, 100, 1000ms
20
- const MAX_TIMEOUT_MS = 60000;
21
- const GRADUATED_INTERVAL_MS = 5000;
22
- const BLOCK_DURATION = 15000; //Set to `15` so SIGINT/SIGTERM can interrupt; set to `0` to BLOCK indefinitely
23
- const TEST_BLOCK_DURATION = 1000; //Set to `1000` so tests can interrupt quickly
24
- const BLOCK_TIME_MS = process.env.NODE_ENV === 'test' ? TEST_BLOCK_DURATION : BLOCK_DURATION;
25
- const SYSTEM_STATUS_CODE = 999;
26
- const UNKNOWN_STATUS_CODE = 500;
27
- const UNKNOWN_STATUS_MESSAGE = 'unknown';
28
- const XCLAIM_DELAY_MS = 1000 * 60; //max time a message can be unacked before it is claimed by another
29
- const XCLAIM_COUNT = 3; //max number of times a message can be claimed by another before it is dead-lettered
30
- const XPENDING_COUNT = 10;
31
-
32
30
  class StreamSignaler {
33
31
  static signalers: Set<StreamSignaler> = new Set();
34
32
  appId: string;
@@ -131,7 +129,7 @@ class StreamSignaler {
131
129
  telemetry.startStreamSpan(input, this.role);
132
130
  output = await this.execStreamLeg(input, stream, id, callback.bind(this));
133
131
  if (output?.status === StreamStatus.ERROR) {
134
- telemetry.setStreamError(`Function Status Code ${ output.code || UNKNOWN_STATUS_CODE }`);
132
+ telemetry.setStreamError(`Function Status Code ${ output.code || STATUS_CODE_UNKNOWN }`);
135
133
  }
136
134
  this.errorCount = 0;
137
135
  } catch (err) {
@@ -171,12 +169,17 @@ class StreamSignaler {
171
169
  await sleepFor(timeout);
172
170
  return await this.publishMessage(input.metadata.topic, {
173
171
  data: input.data,
172
+ //note: retain guid (this is a retry attempt)
174
173
  metadata: { ...input.metadata, try: (input.metadata.try || 0) + 1 },
175
174
  policies: input.policies,
176
175
  }) as string;
177
176
  } else {
178
177
  output = this.structureError(input, output);
179
178
  }
179
+ } else if (typeof output.metadata !== 'object') {
180
+ output.metadata = { ...input.metadata, guid: guid() };
181
+ } else {
182
+ output.metadata.guid = guid();
180
183
  }
181
184
  output.type = StreamDataType.RESPONSE;
182
185
  return await this.publishMessage(null, output as StreamDataResponse) as string;
@@ -203,7 +206,7 @@ class StreamSignaler {
203
206
  if (typeof err.message === 'string') {
204
207
  error.message = err.message;
205
208
  } else {
206
- error.message = UNKNOWN_STATUS_MESSAGE;
209
+ error.message = STATUS_MESSAGE_UNKNOWN;
207
210
  }
208
211
  if (typeof err.stack === 'string') {
209
212
  error.stack = err.stack;
@@ -213,18 +216,18 @@ class StreamSignaler {
213
216
  }
214
217
  return {
215
218
  status: 'error',
216
- code: UNKNOWN_STATUS_CODE,
217
- metadata: { ...input.metadata },
219
+ code: STATUS_CODE_UNKNOWN,
220
+ metadata: { ...input.metadata, guid: guid() },
218
221
  data: error as StreamError
219
222
  } as StreamDataResponse;
220
223
  }
221
224
 
222
225
  structureUnacknowledgedError(input: StreamData) {
223
226
  const message = 'stream message max delivery count exceeded';
224
- const code = SYSTEM_STATUS_CODE;
227
+ const code = STATUS_CODE_UNACKED;
225
228
  const data: StreamError = { message, code };
226
229
  const output: StreamDataResponse = {
227
- metadata: { ...input.metadata },
230
+ metadata: { ...input.metadata, guid: guid() },
228
231
  status: StreamStatus.ERROR,
229
232
  code,
230
233
  data,
@@ -235,9 +238,9 @@ class StreamSignaler {
235
238
  }
236
239
 
237
240
  structureError(input: StreamData, output: StreamDataResponse): StreamDataResponse {
238
- const message = output.data?.message ? output.data?.message.toString() : UNKNOWN_STATUS_MESSAGE;
241
+ const message = output.data?.message ? output.data?.message.toString() : STATUS_MESSAGE_UNKNOWN;
239
242
  const statusCode = output.code || output.data?.code;
240
- const code = isNaN(statusCode as number) ? UNKNOWN_STATUS_CODE : parseInt(statusCode.toString());
243
+ const code = isNaN(statusCode as number) ? STATUS_CODE_UNKNOWN : parseInt(statusCode.toString());
241
244
  const data: StreamError = { message, code };
242
245
  if (typeof output.data?.error === 'object') {
243
246
  data.error = { ...output.data.error };
@@ -245,7 +248,7 @@ class StreamSignaler {
245
248
  return {
246
249
  status: StreamStatus.ERROR,
247
250
  code,
248
- metadata: { ...input.metadata },
251
+ metadata: { ...input.metadata, guid: guid() },
249
252
  data
250
253
  } as StreamDataResponse;
251
254
  }
@@ -307,7 +310,7 @@ class StreamSignaler {
307
310
  // ii) corrupt hardware/network/transport/etc
308
311
  // 3b) system error: Redis unable to accept `xadd` request
309
312
  // 4c) system error: Redis unable to accept `xdel`/`xack` request
310
- this.logger.error('stream-message-max-delivery-count-exceeded', { id, stream, group, consumer, code: SYSTEM_STATUS_CODE, count });
313
+ this.logger.error('stream-message-max-delivery-count-exceeded', { id, stream, group, consumer, code: STATUS_CODE_UNACKED, count });
311
314
  const streamData = reclaimedMessage[0]?.[1]?.[1];
312
315
 
313
316
  //fatal risk point 1 of 3): json is corrupt
@@ -28,6 +28,9 @@ import {
28
28
  import { Transitions } from '../../types/transition';
29
29
  import { formatISODate, getSymKey } from '../../modules/utils';
30
30
  import { ReclaimedMessageType } from '../../types/stream';
31
+ import { JobCompletionOptions, JobInterruptOptions } from '../../types/job';
32
+ import { STATUS_CODE_INTERRUPT } from '../../modules/enums';
33
+ import { GetStateError } from '../../modules/errors';
31
34
 
32
35
  interface AbstractRedisClient {
33
36
  exec(): any;
@@ -371,6 +374,22 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
371
374
  return await this.redisClient[this.commands.hset](key, payload as any);
372
375
  }
373
376
 
377
+ /**
378
+ * Registers jobId with the originJobId that spawned it. In the future,
379
+ * when originJobId is interrupted or expired, the items in the
380
+ * list (added via RPUSH) are LPOPed. If origin was expired, then
381
+ * LPOPed items from the list are likewise expired;
382
+ */
383
+ async setDependency(originJobId: string, topic: string, jobId: string, multi? : U): Promise<any> {
384
+ const privateMulti = multi || this.getMulti();
385
+ const depParams = { appId: this.appId, jobId: originJobId };
386
+ const depKey = this.mintKey(KeyType.JOB_DEPENDENTS, depParams);
387
+ privateMulti[this.commands.rpush](depKey, `expire::${topic}::${jobId}`);
388
+ if (!multi) {
389
+ return await privateMulti.exec();
390
+ }
391
+ }
392
+
374
393
  async setStats(jobKey: string, jobId: string, dateTime: string, stats: StatsType, appVersion: AppVID, multi? : U): Promise<any> {
375
394
  const params: KeyStoreParams = { appId: appVersion.id, jobId, jobKey, dateTime };
376
395
  const privateMulti = multi || this.getMulti();
@@ -477,8 +496,8 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
477
496
  }
478
497
 
479
498
  /**
480
- * returns custom search fields and values. The fields param
481
- * should not prefix items with an underscore.
499
+ * Returns custom search fields and values.
500
+ * NOTE: The `fields` param should NOT prefix items with an underscore.
482
501
  */
483
502
  async getQueryState(jobId: string, fields: string[]): Promise<StringAnyType> {
484
503
  const key = this.mintKey(KeyType.JOB_STATE, { appId: this.appId, jobId });
@@ -520,7 +539,7 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
520
539
  }
521
540
  return [state, status];
522
541
  } else {
523
- throw new Error(`Job ${jobId} not found`);
542
+ throw new GetStateError(jobId);
524
543
  }
525
544
  }
526
545
 
@@ -745,7 +764,27 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
745
764
  }
746
765
  }
747
766
 
748
- async registerTimeHook(jobId: string, activityId: string, type: 'sleep'|'expire'|'cron', deletionTime: number, multi?: U): Promise<void> {
767
+ /**
768
+ * register the descendants of an expired origin flow to be
769
+ * expired at a future date; options indicate whether this
770
+ * is a standard `expire` or an `interrupt`
771
+ */
772
+ async registerExpireJob(jobId: string, deletionTime: number, options: JobCompletionOptions): Promise<void> {
773
+ const depParams = { appId: this.appId, jobId };
774
+ const depKey = this.mintKey(KeyType.JOB_DEPENDENTS, depParams);
775
+ const context = options.interrupt ? 'INTERRUPT' : 'EXPIRE';
776
+ const depKeyContext = `::${context}::${depKey}`;
777
+ const zsetKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId });
778
+ await this.zAdd(zsetKey, deletionTime.toString(), depKeyContext);
779
+ }
780
+
781
+ /**
782
+ * registers a hook activity to be awakened (uses ZSET to
783
+ * store the 'sleep group' and LIST to store the events
784
+ * for the given sleep group. Sleep groups are
785
+ * organized into 'n'-second blocks (LISTS))
786
+ */
787
+ async registerTimeHook(jobId: string, activityId: string, type: 'sleep'|'expire'|'interrupt', deletionTime: number, multi?: U): Promise<void> {
749
788
  const listKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId, timeValue: deletionTime });
750
789
  const timeEvent = `${type}::${activityId}::${jobId}`
751
790
  const len = await (multi || this.redisClient)[this.commands.rpush](listKey, timeEvent);
@@ -755,21 +794,90 @@ abstract class StoreService<T, U extends AbstractRedisClient> {
755
794
  }
756
795
  }
757
796
 
758
- async getNextTimeJob(listKey?: string): Promise<[listKey: string, jobId: string, activityId: string] | void> {
797
+ async getNextTimeJob(listKey?: string): Promise<[listKey: string, jobId: string, activityId: string, type: 'sleep'|'expire'|'interrupt'] | void> {
759
798
  const zsetKey = this.mintKey(KeyType.TIME_RANGE, { appId: this.appId });
760
- const now = Date.now();
761
- listKey = listKey || await this.zRangeByScore(zsetKey, 0, now);
799
+ listKey = listKey || await this.zRangeByScore(zsetKey, 0, Date.now());
762
800
  if (listKey) {
763
- const timeEvent = await this.redisClient[this.commands.lpop](listKey);
801
+ const [pType, pKey] = this.resolveKeyContext(listKey);
802
+ const timeEvent = await this.redisClient[this.commands.lpop](pKey);
764
803
  if (timeEvent) {
765
- //placeholder: there are 3 time-related event triggers: sleep, expire, cron
766
- const [type, activityId, ...jobId] = timeEvent.split('::');
767
- return [listKey, jobId.join('::'), activityId];
804
+ //there are 3 time-related event triggers: sleep, expire, interrupt
805
+ const [_type, activityId, ...jobId] = timeEvent.split('::');
806
+ return [listKey, jobId.join('::'), activityId, pType];
768
807
  }
769
808
  await this.redisClient[this.commands.zrem](zsetKey, listKey);
770
809
  }
771
810
  }
772
811
 
812
+ /**
813
+ * when processing time jobs, the target LIST ID returned
814
+ * from the ZSET query can be prefixed to denote what to
815
+ * do with the work list. (not everything is known in advance,
816
+ * so the ZSET key defines HOW to approach the work in the
817
+ * generic LIST (lists typically contain target job ids)
818
+ * @param {string} listKey - for example `::INTERRUPT::job123` or `job123`
819
+ */
820
+ resolveKeyContext(listKey: string): [('sleep'|'expire'|'interrupt'), string] {
821
+ if (listKey.startsWith('::INTERRUPT')) {
822
+ return ['interrupt', listKey.split('::')[2]];
823
+ } else if (listKey.startsWith('::EXPIRE')) {
824
+ return ['expire', listKey.split('::')[2]];
825
+ } else {
826
+ return ['sleep', listKey];
827
+ }
828
+ }
829
+
830
+ /**
831
+ * Interrupts a job and sets sets a job error (410), if 'throw'!=false.
832
+ * This method is called by the engine and not by an activity and is
833
+ * followed by a call to execute job completion/cleanup tasks
834
+ * associated with a job completion event.
835
+ */
836
+ async interrupt(topic: string, jobId: string, options: JobInterruptOptions = {}): Promise<void> {
837
+ try {
838
+ //verify job exists
839
+ const status = await this.getStatus(jobId, this.appId);
840
+ if (status <= 0) {
841
+ //verify still active; job already completed
842
+ throw new Error(`Job ${jobId} already completed`);
843
+ }
844
+ //decrement job status (:) by 1bil
845
+ const amount = -1_000_000_000;
846
+ const jobKey = this.mintKey(KeyType.JOB_STATE, { appId: this.appId, jobId });
847
+ const result = await this.redisClient[this.commands.hincrbyfloat](jobKey, ':', amount);
848
+ if (result <= amount) {
849
+ //verify active state; job already interrupted
850
+ throw new Error(`Job ${jobId} already completed`);
851
+ }
852
+ //persist the error unless specifically told not to
853
+ if (options.throw !== false) {
854
+ const errKey = `metadata/err`; //job errors are stored at the path `metadata/err`
855
+ const symbolNames = [`$${topic}`]; //the symbol for `metadata/err` is in redis and stored using the job topic
856
+ const symKeys = await this.getSymbolKeys(symbolNames);
857
+ const symVals = await this.getSymbolValues();
858
+ this.serializer.resetSymbols(symKeys, symVals, {});
859
+
860
+ //persists the standard 410 error (job is `gone`)
861
+ const err = JSON.stringify({
862
+ code: STATUS_CODE_INTERRUPT,
863
+ message: options.reason ?? `job [${jobId}] interrupted`,
864
+ job_id: jobId
865
+ });
866
+
867
+ const payload = { [errKey]: amount.toString() }
868
+ const hashData = this.serializer.package(payload, symbolNames);
869
+ const errSymbol = Object.keys(hashData)[0];
870
+ await this.redisClient[this.commands.hset](jobKey, errSymbol, err);
871
+ }
872
+ } catch (e) {
873
+ if (!options.suppress) {
874
+ throw e;
875
+ } else {
876
+ this.logger.debug('suppressed-interrupt', { message: e.message })
877
+ }
878
+ }
879
+ }
880
+
773
881
  async scrub(jobId: string) {
774
882
  const jobKey = this.mintKey(KeyType.JOB_STATE, { appId: this.appId, jobId });
775
883
  await this.redisClient[this.commands.del](jobKey);
@@ -1,13 +1,12 @@
1
+ import {
2
+ EXPIRE_DURATION,
3
+ FIDELITY_SECONDS } from '../../modules/enums';
4
+ import { XSleepFor, sleepFor } from '../../modules/utils';
1
5
  import { ILogger } from '../logger';
2
6
  import { StoreService } from '../store';
3
- import { RedisClient, RedisMulti } from '../../types/redis';
4
7
  import { HookInterface } from '../../types/hook';
5
- import { XSleepFor, sleepFor } from '../../modules/utils';
6
-
7
- //system timer granularity limit (task queues organize)
8
- const FIDELITY_SECONDS = 15; //note: this can be reduced using 'watch' or scout role
9
- //default resolution/fidelity when expiring
10
- const EXPIRATION_FIDELITY_SECONDS = 60;
8
+ import { JobCompletionOptions } from '../../types/job';
9
+ import { RedisClient, RedisMulti } from '../../types/redis';
11
10
 
12
11
  class TaskService {
13
12
  store: StoreService<RedisClient, RedisMulti>;
@@ -43,25 +42,26 @@ class TaskService {
43
42
  await this.store.addTaskQueues(keys);
44
43
  }
45
44
 
46
- async registerJobForCleanup(jobId: string, inSeconds = EXPIRATION_FIDELITY_SECONDS): Promise<void> {
47
- if (inSeconds > -1) {
45
+ async registerJobForCleanup(jobId: string, inSeconds = EXPIRE_DURATION, options: JobCompletionOptions): Promise<void> {
46
+ if (inSeconds > 0) {
48
47
  await this.store.expireJob(jobId, inSeconds);
48
+ const expireTimeSlot = Math.floor((Date.now() + (inSeconds * 1000)) / (FIDELITY_SECONDS * 1000)) * (FIDELITY_SECONDS * 1000); //n second awaken groups
49
+ await this.store.registerExpireJob(jobId, expireTimeSlot, options);
49
50
  }
50
51
  }
51
52
 
52
- async registerTimeHook(jobId: string, activityId: string, type: 'sleep'|'expire'|'cron', inSeconds = FIDELITY_SECONDS, multi?: RedisMulti): Promise<void> {
53
- const awakenTimeSlot = Math.floor((Date.now() + inSeconds * 1000) / (FIDELITY_SECONDS * 1000)) * (FIDELITY_SECONDS * 1000); //n second awaken groups
53
+ async registerTimeHook(jobId: string, activityId: string, type: 'sleep'|'expire'|'interrupt', inSeconds = FIDELITY_SECONDS, multi?: RedisMulti): Promise<void> {
54
+ const awakenTimeSlot = Math.floor((Date.now() + (inSeconds * 1000)) / (FIDELITY_SECONDS * 1000)) * (FIDELITY_SECONDS * 1000); //n second awaken groups
54
55
  await this.store.registerTimeHook(jobId, activityId, type, awakenTimeSlot, multi);
55
56
  }
56
57
 
57
- //todo: need 'scout' role in quorum to check for this and then alert the quorum to get to work
58
- async processTimeHooks(timeEventCallback: (jobId: string, activityId: string) => Promise<void>, listKey?: string): Promise<void> {
58
+ async processTimeHooks(timeEventCallback: (jobId: string, activityId: string, type: 'sleep'|'expire'|'interrupt') => Promise<void>, listKey?: string): Promise<void> {
59
59
  try {
60
- const job = await this.store.getNextTimeJob(listKey);
61
- if (job) {
62
- const [listKey, jobId, activityId] = job;
63
- await timeEventCallback(jobId, activityId);
64
- await sleepFor(0);
60
+ const timeJob = await this.store.getNextTimeJob(listKey);
61
+ if (timeJob) {
62
+ const [listKey, jobId, activityId, type] = timeJob;
63
+ await timeEventCallback(jobId, activityId, type);
64
+ await sleepFor(0);
65
65
  this.processTimeHooks(timeEventCallback, listKey);
66
66
  } else {
67
67
  let sleep = XSleepFor(FIDELITY_SECONDS * 1000);
package/types/activity.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { MetricTypes } from "./stats";
2
2
  import { StreamRetryPolicy } from "./stream";
3
3
 
4
- type ActivityExecutionType = 'trigger' | 'await' | 'worker' | 'activity' | 'emit' | 'iterate' | 'cycle' | 'signal' | 'hook';
4
+ type ActivityExecutionType = 'trigger' | 'await' | 'worker' | 'activity' | 'emit' | 'interrupt' | 'cycle' | 'signal' | 'hook';
5
5
 
6
6
  type Consumes = Record<string, string[]>;
7
7
 
@@ -17,7 +17,7 @@ interface BaseActivity {
17
17
  telemetry?: Record<string, any>;
18
18
  emit?: boolean; //if true, the activity will emit a message to the `publishes` topic immediately before transitioning to adjacent activities
19
19
  sleep?: number; //@pipe /in seconds
20
- expire?: number; //-1 forever (15 seconds default); todo: make globally configurable
20
+ expire?: number; //-1 forever; 0 persists the flow until the parent flow that expired it is dismissed; 15 seconds is the default
21
21
  retry?: StreamRetryPolicy
22
22
  cycle?: boolean; //if true, the `notary` will leave leg 2 open, so it can be re/cycled
23
23
  collationInt?: number; //compiler
@@ -37,10 +37,30 @@ interface Measure {
37
37
  }
38
38
 
39
39
  interface TriggerActivityStats {
40
+ /**
41
+ * parent job; including this allows the parent's
42
+ * expiration/interruption events to cascade; set
43
+ * `expire` in the YAML for the dependent graph
44
+ * to 0 and provide the parent for dependent,
45
+ * cascading interruption and cleanup
46
+ */
47
+ parent?: string;
40
48
  id?: { [key: string]: unknown } | string;
41
49
  key?: { [key: string]: unknown } | string;
42
- granularity?: string; //return 'infinity' to disable; default behavior is to always segment keys by time to ensure indexes (Redis LIST) never grow unbounded as a default behavior; for now, 5m is default and infinity can be set to override
43
- measures?: Measure[]; //what to capture
50
+ /**
51
+ * @deprecated
52
+ * return 'infinity' to disable; default behavior
53
+ * is to always segment keys by time to ensure
54
+ * indexes (Redis LIST) never grow unbounded
55
+ * as a default behavior; for now, 5m is default
56
+ * and infinity can be set to override
57
+ */
58
+ granularity?: string;
59
+ /**
60
+ * @deprecated
61
+ * what to capture
62
+ */
63
+ measures?: Measure[];
44
64
  }
45
65
 
46
66
  interface TriggerActivity extends BaseActivity {
@@ -82,11 +102,21 @@ interface SignalActivity extends BaseActivity {
82
102
  code?: number; //202, 200 (default)
83
103
  }
84
104
 
85
- interface IterateActivity extends BaseActivity {
86
- type: 'iterate';
105
+ interface InterruptActivity extends BaseActivity {
106
+ type: 'interrupt';
107
+ /** Optional Reason; will be used as the error `message` when thrown; NOTE: 410 is the error `code` */
108
+ reason?: string;
109
+ /** default is `true` (throw JobInterrupted error upon interrupting) */
110
+ throw?: boolean;
111
+ /** TODO: // default is `false` (do not interrupt child jobs) */
112
+ descend?: boolean;
113
+ /** target job id (if not present the current job will be targeted) */
114
+ target?: string;
115
+ /** topic to publish the interrupt message (if not present the current job topic will be used) */
116
+ topic?: string;
87
117
  }
88
118
 
89
- type ActivityType = BaseActivity | TriggerActivity | AwaitActivity | WorkerActivity | IterateActivity | HookActivity;
119
+ type ActivityType = BaseActivity | TriggerActivity | AwaitActivity | WorkerActivity | InterruptActivity | HookActivity | SignalActivity | CycleActivity;
90
120
 
91
121
  type ActivityData = Record<string, any>;
92
122
  type ActivityMetadata = {
@@ -133,7 +163,7 @@ export {
133
163
  HookActivity,
134
164
  SignalActivity,
135
165
  BaseActivity,
136
- IterateActivity,
166
+ InterruptActivity,
137
167
  TriggerActivity,
138
168
  WorkerActivity
139
169
  };
package/types/durable.ts CHANGED
@@ -60,10 +60,12 @@ type WorkflowOptions = {
60
60
  entity?: string; //If invoking a workflow, passing 'entity' will apply the value as the workflowName, taskQueue, and prefix, ensuring the FT.SEARCH index is properly scoped. This is a convenience method but limits options.
61
61
  workflowName?: string; //the name of the user's workflow function; optional if 'entity' is provided
62
62
  parentWorkflowId?: string; //system reserved; the id of the parent; if present the flow will not self-clean until the parent that spawned it self-cleans
63
+ originJobId?: string; //system reserved;
63
64
  workflowTrace?: string;
64
65
  workflowSpan?: string;
65
66
  search?: WorkflowSearchOptions
66
67
  config?: WorkflowConfig;
68
+ expire?: number; //default is 3seconds; time before completed jobs and dependents are expired/scrubbed/removed
67
69
  }
68
70
 
69
71
  type HookOptions = {
@@ -80,8 +82,8 @@ type HookOptions = {
80
82
  type SignalOptions = {
81
83
  taskQueue: string;
82
84
  data: Record<string, any>; //input data (any serializable object)
83
- workflowId: string; //execution id (the job id)
84
- workflowName?: string; //the name of the user's workflow function
85
+ workflowId: string; //execution id (the job id)
86
+ workflowName?: string; //the name of the user's workflow function
85
87
  }
86
88
 
87
89
  type ActivityWorkflowDataType = {
@@ -95,6 +97,8 @@ type WorkflowDataType = {
95
97
  arguments: any[];
96
98
  workflowId: string;
97
99
  workflowTopic: string;
100
+ workflowDimension?: string; //is present if hook (not main workflow)
101
+ originJobId?: string; //is present if there is an originating ancestor job (should rename to originJobId)
98
102
  }
99
103
 
100
104
  type MeshOSClassConfig = {
@@ -105,8 +109,8 @@ type MeshOSClassConfig = {
105
109
  }
106
110
 
107
111
  type MeshOSConfig = {
108
- id?: string; //guid for the workflow when instancing
109
- await?: boolean; //default is false; must explicitly send true to await the final result
112
+ id?: string; //guid for the workflow when instancing
113
+ await?: boolean; //default is false; must explicitly send true to await the final result
110
114
  taskQueue?: string; //optional target queue isolate for the function
111
115
  }
112
116
 
package/types/index.ts CHANGED
@@ -12,7 +12,7 @@ export {
12
12
  CycleActivity,
13
13
  HookActivity,
14
14
  WorkerActivity,
15
- IterateActivity,
15
+ InterruptActivity,
16
16
  SignalActivity,
17
17
  TriggerActivity,
18
18
  TriggerActivityStats } from './activity';
package/types/job.ts CHANGED
@@ -44,6 +44,19 @@ type JobState = {
44
44
  };
45
45
  };
46
46
 
47
+ type JobInterruptOptions = {
48
+ /** Optional reason when throwing the error */
49
+ reason?: string;
50
+ /** default is `true` when `undefined` (throw JobInterrupted/410 error) */
51
+ throw?: boolean;
52
+ /** default behavior is `false` when `undefined` (do NOT interrupt child jobs) */
53
+ descend?: boolean;
54
+ /** default is false; if true, errors related to inactivation (like overage...already inactive) are suppressed/ignored */
55
+ suppress?: boolean;
56
+ /** how long to wait in seconds before fully expiring/removing the hash from Redis; the job is inactive, but can remain in the cache indefinitely. minimum 1 second.*/
57
+ expire?: number;
58
+ };
59
+
47
60
  //format when publishing job meta/data on the wire when it completes
48
61
  type JobOutput = {
49
62
  metadata: JobMetadata;
@@ -56,4 +69,20 @@ type PartialJobState = {
56
69
  data: JobData;
57
70
  };
58
71
 
59
- export { JobState, JobStatus, JobData, JobsData, JobMetadata, PartialJobState, JobOutput };
72
+ type JobCompletionOptions = {
73
+ emit?: boolean; //default false
74
+ interrupt?: boolean; //default undefined
75
+ expire?: number; // in seconds to wait before deleting/expiring job hash
76
+ }
77
+
78
+ export {
79
+ JobCompletionOptions,
80
+ JobInterruptOptions,
81
+ JobData,
82
+ JobsData,
83
+ JobMetadata,
84
+ JobOutput,
85
+ JobState,
86
+ JobStatus,
87
+ PartialJobState,
88
+ };
package/types/quorum.ts CHANGED
@@ -1,13 +1,5 @@
1
1
  import { JobOutput } from "./job";
2
2
 
3
- /**
4
- * The types in this file are used to define those messages that are sent
5
- * to hotmesh client instances when a new version is about to be activated.
6
- * These messages serve to coordinate the cache invalidation and switch-over
7
- * to the new version without any downtime and a coordinating parent server.
8
- */
9
- export type QuorumMessage = PingMessage | PongMessage | ActivateMessage | WorkMessage | JobMessage | ThrottleMessage;
10
-
11
3
  //used for coordination like version activation
12
4
  export interface PingMessage {
13
5
  type: 'ping';
@@ -19,6 +11,11 @@ export interface WorkMessage {
19
11
  originator: string; //guid
20
12
  }
21
13
 
14
+ export interface CronMessage {
15
+ type: 'cron';
16
+ originator: string; //guid
17
+ }
18
+
22
19
  //used for coordination like version activation
23
20
  export interface PongMessage {
24
21
  type: 'pong';
@@ -57,3 +54,11 @@ export interface SubscriptionCallback {
57
54
  export interface QuorumMessageCallback {
58
55
  (topic: string, message: QuorumMessage): void;
59
56
  }
57
+
58
+ /**
59
+ * The types in this file are used to define those messages that are sent
60
+ * to hotmesh client instances when a new version is about to be activated.
61
+ * These messages serve to coordinate the cache invalidation and switch-over
62
+ * to the new version without any downtime and a coordinating parent server.
63
+ */
64
+ export type QuorumMessage = PingMessage | PongMessage | ActivateMessage | WorkMessage | JobMessage | ThrottleMessage | CronMessage;
package/types/stream.ts CHANGED
@@ -27,10 +27,13 @@ export enum StreamDataType {
27
27
  WORKER = 'worker',
28
28
  RESPONSE = 'response', //worker response
29
29
  TRANSITION = 'transition',
30
+ SIGNAL = 'signal',
31
+ INTERRUPT = 'interrupt',
30
32
  }
31
33
 
32
34
  export interface StreamData {
33
35
  metadata: {
36
+ guid: string; //every message is minted with a guid to distinguish retries from new messages
34
37
  topic?: string;
35
38
  jid?: string; //is optonal if type is WEBHOOK
36
39
  dad?: string; //dimensional address
@@ -1,9 +0,0 @@
1
- import { EngineService } from '../engine';
2
- import { Activity, ActivityType } from './activity';
3
- import { ActivityData, ActivityMetadata, IterateActivity } from '../../types/activity';
4
- declare class Iterate extends Activity {
5
- config: IterateActivity;
6
- constructor(config: ActivityType, data: ActivityData, metadata: ActivityMetadata, hook: ActivityData | null, engine: EngineService);
7
- mapInputData(): Promise<void>;
8
- }
9
- export { Iterate };
@@ -1,13 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Iterate = void 0;
4
- const activity_1 = require("./activity");
5
- class Iterate extends activity_1.Activity {
6
- constructor(config, data, metadata, hook, engine) {
7
- super(config, data, metadata, hook, engine);
8
- }
9
- async mapInputData() {
10
- this.logger.info('iterate-map-input-data');
11
- }
12
- }
13
- exports.Iterate = Iterate;
@@ -1,26 +0,0 @@
1
- import { EngineService } from '../engine';
2
- import { Activity, ActivityType } from './activity';
3
- import {
4
- ActivityData,
5
- ActivityMetadata,
6
- IterateActivity } from '../../types/activity';
7
-
8
- class Iterate extends Activity {
9
- config: IterateActivity;
10
-
11
- constructor(
12
- config: ActivityType,
13
- data: ActivityData,
14
- metadata: ActivityMetadata,
15
- hook: ActivityData | null,
16
- engine: EngineService
17
- ) {
18
- super(config, data, metadata, hook, engine);
19
- }
20
-
21
- async mapInputData(): Promise<void> {
22
- this.logger.info('iterate-map-input-data');
23
- }
24
- }
25
-
26
- export { Iterate };