@hotmeshio/hotmesh 0.0.12 → 0.0.14

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 (82) hide show
  1. package/README.md +2 -2
  2. package/build/modules/errors.d.ts +22 -1
  3. package/build/modules/errors.js +28 -1
  4. package/build/modules/utils.d.ts +2 -1
  5. package/build/modules/utils.js +5 -1
  6. package/build/package.json +7 -2
  7. package/build/services/activities/activity.d.ts +2 -0
  8. package/build/services/activities/activity.js +16 -10
  9. package/build/services/activities/await.d.ts +2 -6
  10. package/build/services/activities/await.js +12 -75
  11. package/build/services/activities/cycle.js +2 -2
  12. package/build/services/activities/index.d.ts +2 -2
  13. package/build/services/activities/index.js +2 -2
  14. package/build/services/activities/signal.d.ts +16 -0
  15. package/build/services/activities/signal.js +94 -0
  16. package/build/services/activities/trigger.js +4 -3
  17. package/build/services/activities/worker.d.ts +2 -1
  18. package/build/services/activities/worker.js +11 -6
  19. package/build/services/compiler/deployer.js +3 -1
  20. package/build/services/durable/client.d.ts +3 -2
  21. package/build/services/durable/client.js +39 -21
  22. package/build/services/durable/factory.d.ts +22 -18
  23. package/build/services/durable/factory.js +722 -50
  24. package/build/services/durable/handle.d.ts +1 -0
  25. package/build/services/durable/handle.js +5 -1
  26. package/build/services/durable/worker.d.ts +3 -8
  27. package/build/services/durable/worker.js +75 -73
  28. package/build/services/durable/workflow.d.ts +5 -0
  29. package/build/services/durable/workflow.js +93 -24
  30. package/build/services/engine/index.d.ts +6 -6
  31. package/build/services/engine/index.js +25 -15
  32. package/build/services/hotmesh/index.d.ts +2 -1
  33. package/build/services/hotmesh/index.js +3 -1
  34. package/build/services/mapper/index.js +1 -1
  35. package/build/services/pipe/functions/array.d.ts +1 -0
  36. package/build/services/pipe/functions/array.js +3 -0
  37. package/build/services/reporter/index.js +9 -2
  38. package/build/services/signaler/store.js +8 -3
  39. package/build/services/signaler/stream.js +3 -3
  40. package/build/services/store/clients/ioredis.js +15 -15
  41. package/build/services/store/clients/redis.js +18 -18
  42. package/build/services/store/index.d.ts +1 -1
  43. package/build/services/store/index.js +11 -3
  44. package/build/services/task/index.js +3 -3
  45. package/build/types/activity.d.ts +15 -6
  46. package/build/types/durable.d.ts +15 -2
  47. package/build/types/index.d.ts +2 -2
  48. package/build/types/stats.d.ts +1 -0
  49. package/modules/errors.ts +35 -0
  50. package/modules/utils.ts +5 -1
  51. package/package.json +7 -2
  52. package/services/activities/activity.ts +19 -9
  53. package/services/activities/await.ts +14 -90
  54. package/services/activities/cycle.ts +2 -2
  55. package/services/activities/index.ts +2 -2
  56. package/services/activities/signal.ts +124 -0
  57. package/services/activities/trigger.ts +4 -3
  58. package/services/activities/worker.ts +13 -13
  59. package/services/compiler/deployer.ts +3 -1
  60. package/services/durable/client.ts +48 -23
  61. package/services/durable/factory.ts +723 -49
  62. package/services/durable/handle.ts +6 -1
  63. package/services/durable/worker.ts +92 -79
  64. package/services/durable/workflow.ts +95 -25
  65. package/services/engine/index.ts +33 -24
  66. package/services/hotmesh/index.ts +7 -4
  67. package/services/mapper/index.ts +1 -1
  68. package/services/pipe/functions/array.ts +4 -0
  69. package/services/reporter/index.ts +10 -2
  70. package/services/signaler/store.ts +8 -3
  71. package/services/signaler/stream.ts +3 -3
  72. package/services/store/clients/ioredis.ts +15 -15
  73. package/services/store/clients/redis.ts +18 -18
  74. package/services/store/index.ts +12 -3
  75. package/services/task/index.ts +3 -3
  76. package/types/activity.ts +16 -7
  77. package/types/durable.ts +18 -1
  78. package/types/index.ts +2 -1
  79. package/types/stats.ts +1 -0
  80. package/build/services/activities/emit.d.ts +0 -9
  81. package/build/services/activities/emit.js +0 -13
  82. package/services/activities/emit.ts +0 -25
@@ -18,6 +18,7 @@ declare class HotMeshService {
18
18
  verifyAndSetNamespace(namespace?: string): void;
19
19
  verifyAndSetAppId(appId: string): void;
20
20
  static init(config: HotMeshConfig): Promise<HotMeshService>;
21
+ static guid(): string;
21
22
  initEngine(config: HotMeshConfig, logger: ILogger): Promise<void>;
22
23
  initQuorum(config: HotMeshConfig, engine: EngineService, logger: ILogger): Promise<void>;
23
24
  initWorkers(config: HotMeshConfig, logger: ILogger): Promise<void>;
@@ -37,7 +38,7 @@ declare class HotMeshService {
37
38
  getIds(topic: string, query: JobStatsInput, queryFacets?: any[]): Promise<IdsResponse>;
38
39
  resolveQuery(topic: string, query: JobStatsInput): Promise<GetStatsOptions>;
39
40
  scrub(jobId: string): Promise<void>;
40
- hook(topic: string, data: JobData, dad?: string): Promise<JobStatus | void>;
41
+ hook(topic: string, data: JobData, dad?: string): Promise<string>;
41
42
  hookAll(hookTopic: string, data: JobData, query: JobStatsInput, queryFacets?: string[]): Promise<string[]>;
42
43
  stop(): Promise<void>;
43
44
  compress(terms: string[]): Promise<boolean>;
@@ -48,6 +48,9 @@ class HotMeshService {
48
48
  await instance.initWorkers(config, instance.logger);
49
49
  return instance;
50
50
  }
51
+ static guid() {
52
+ return (0, nanoid_1.nanoid)();
53
+ }
51
54
  async initEngine(config, logger) {
52
55
  if (config.engine) {
53
56
  await connector_1.ConnectorService.initRedisClients(config.engine.redis?.class, config.engine.redis?.options, config.engine);
@@ -117,7 +120,6 @@ class HotMeshService {
117
120
  }
118
121
  // ****** `HOOK` ACTIVITY RE-ENTRY POINT ******
119
122
  async hook(topic, data, dad) {
120
- //return collation int
121
123
  return await this.engine?.hook(topic, data, dad);
122
124
  }
123
125
  async hookAll(hookTopic, data, query, queryFacets = []) {
@@ -49,7 +49,7 @@ class MapperService {
49
49
  if (typeof transitionRule === 'boolean') {
50
50
  return transitionRule;
51
51
  }
52
- if (code.toString() === (transitionRule.code || 200).toString()) {
52
+ if ((Array.isArray(transitionRule.code) && transitionRule.code.includes(code || 200)) || code.toString() === (transitionRule.code || 200).toString()) {
53
53
  if (!transitionRule.match) {
54
54
  return true;
55
55
  }
@@ -1,5 +1,6 @@
1
1
  declare class ArrayHandler {
2
2
  get(array: any[], index: number): any;
3
+ length(array: any[]): any;
3
4
  concat(array1: any[], array2: any[]): any[];
4
5
  every(array: any[], callback: (value: any, index: number, array: any[]) => boolean): boolean;
5
6
  filter(array: any[], callback: (value: any, index: number, array: any[]) => boolean): any[];
@@ -5,6 +5,9 @@ class ArrayHandler {
5
5
  get(array, index) {
6
6
  return array[index];
7
7
  }
8
+ length(array) {
9
+ return array?.length;
10
+ }
8
11
  concat(array1, array2) {
9
12
  return array1.concat(array2);
10
13
  }
@@ -20,12 +20,16 @@ class ReporterService {
20
20
  return statsResponse;
21
21
  }
22
22
  validateOptions(options) {
23
- const { start, end, range } = options;
24
- if (start && end && range || !start && !end && !range) {
23
+ const { start, end, range, granularity } = options;
24
+ if (granularity !== 'infinity' && (start && end && range || !start && !end && !range)) {
25
25
  throw new Error('Invalid combination of start, end, and range values. Provide either start+end, end+range, or start+range.');
26
26
  }
27
27
  }
28
28
  generateDateTimeSets(granularity, range, end, start) {
29
+ if (granularity === 'infinity') {
30
+ //if granularity is infinity, it means a date/time sequence/slice is not used to further segment the statistics
31
+ return ['0'];
32
+ }
29
33
  if (!range) {
30
34
  //pluck just a single value when no range provided
31
35
  range = '0m';
@@ -155,6 +159,9 @@ class ReporterService {
155
159
  return segments;
156
160
  }
157
161
  isoTimestampFromKeyTimestamp(hashKey) {
162
+ if (hashKey.endsWith(':')) {
163
+ return '0';
164
+ }
158
165
  const keyTimestamp = hashKey.slice(-12);
159
166
  const year = keyTimestamp.slice(0, 4);
160
167
  const month = keyTimestamp.slice(4, 6);
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.StoreSignaler = void 0;
4
+ const pipe_1 = require("../pipe");
4
5
  class StoreSignaler {
5
6
  constructor(store, logger) {
6
7
  this.store = store;
@@ -13,10 +14,12 @@ class StoreSignaler {
13
14
  async registerWebHook(topic, context, multi) {
14
15
  const hookRule = await this.getHookRule(topic);
15
16
  if (hookRule) {
17
+ const mapExpression = hookRule.conditions.match[0].expected;
18
+ const resolved = pipe_1.Pipe.resolve(mapExpression, context);
16
19
  const jobId = context.metadata.jid;
17
20
  const hook = {
18
21
  topic,
19
- resolved: jobId,
22
+ resolved,
20
23
  jobId,
21
24
  };
22
25
  await this.store.setHookSignal(hook, multi);
@@ -29,8 +32,10 @@ class StoreSignaler {
29
32
  async processWebHookSignal(topic, data) {
30
33
  const hookRule = await this.getHookRule(topic);
31
34
  if (hookRule) {
32
- //todo: use the rule to generate `resolved`
33
- const resolved = data.id;
35
+ //NOTE: both formats are supported: $self.hook.data OR $hook.data
36
+ const context = { $self: { hook: { data } }, $hook: { data } };
37
+ const mapExpression = hookRule.conditions.match[0].actual;
38
+ const resolved = pipe_1.Pipe.resolve(mapExpression, context);
34
39
  const jobId = await this.store.getHookSignal(topic, resolved);
35
40
  return jobId;
36
41
  }
@@ -120,9 +120,9 @@ class StreamSignaler {
120
120
  try {
121
121
  output = await callback(input);
122
122
  }
123
- catch (err) {
124
- this.logger.error(`stream-call-function-error`, { stream, id, err });
125
- output = this.structureUnhandledError(input, err);
123
+ catch (error) {
124
+ this.logger.error(`stream-call-function-error`, { error });
125
+ output = this.structureUnhandledError(input, error);
126
126
  }
127
127
  return output;
128
128
  }
@@ -51,45 +51,45 @@ class IORedisStoreService extends index_1.StoreService {
51
51
  try {
52
52
  return await (multi || this.redisClient).xadd(key, id, messageId, messageValue);
53
53
  }
54
- catch (err) {
55
- this.logger.error(`Error publishing 'xadd'; key: ${key}`, err);
56
- throw err;
54
+ catch (error) {
55
+ this.logger.error(`Error publishing 'xadd'; key: ${key}`, { error });
56
+ throw error;
57
57
  }
58
58
  }
59
59
  async xpending(key, group, start, end, count, consumer) {
60
60
  try {
61
61
  return await this.redisClient.xpending(key, group, start, end, count, consumer);
62
62
  }
63
- catch (err) {
64
- this.logger.error(`Error in retrieving pending messages for [stream ${key}], [group ${group}]`, err);
65
- throw err;
63
+ catch (error) {
64
+ this.logger.error(`Error in retrieving pending messages for [stream ${key}], [group ${group}]`, { error });
65
+ throw error;
66
66
  }
67
67
  }
68
68
  async xclaim(key, group, consumer, minIdleTime, id, ...args) {
69
69
  try {
70
70
  return await this.redisClient.xclaim(key, group, consumer, minIdleTime, id, ...args);
71
71
  }
72
- catch (err) {
73
- this.logger.error(`Error in claiming message with id: ${id} in group: ${group} for key: ${key}`, err);
74
- throw err;
72
+ catch (error) {
73
+ this.logger.error(`Error in claiming message with id: ${id} in group: ${group} for key: ${key}`, { error });
74
+ throw error;
75
75
  }
76
76
  }
77
77
  async xack(key, group, id, multi) {
78
78
  try {
79
79
  return await (multi || this.redisClient).xack(key, group, id);
80
80
  }
81
- catch (err) {
82
- this.logger.error(`Error in acknowledging messages in group: ${group} for key: ${key}`, err);
83
- throw err;
81
+ catch (error) {
82
+ this.logger.error(`Error in acknowledging messages in group: ${group} for key: ${key}`, { error });
83
+ throw error;
84
84
  }
85
85
  }
86
86
  async xdel(key, id, multi) {
87
87
  try {
88
88
  return await (multi || this.redisClient).xdel(key, id);
89
89
  }
90
- catch (err) {
91
- this.logger.error(`Error in deleting messages with id: ${id} for key: ${key}`, err);
92
- throw err;
90
+ catch (error) {
91
+ this.logger.error(`Error in deleting messages with id: ${id} for key: ${key}`, { error });
92
+ throw error;
93
93
  }
94
94
  }
95
95
  }
@@ -63,10 +63,10 @@ class RedisStoreService extends index_1.StoreService {
63
63
  try {
64
64
  return (await this.redisClient.sendCommand(['XGROUP', 'CREATE', key, groupName, id, ...args])) === 1;
65
65
  }
66
- catch (err) {
66
+ catch (error) {
67
67
  const streamType = mkStream === 'MKSTREAM' ? 'with MKSTREAM' : 'without MKSTREAM';
68
- this.logger.warn(`x-group-error ${streamType} for key: ${key} and group: ${groupName}`, err);
69
- throw err;
68
+ this.logger.warn(`x-group-error ${streamType} for key: ${key} and group: ${groupName}`, { error });
69
+ throw error;
70
70
  }
71
71
  }
72
72
  async xadd(key, id, ...args) {
@@ -77,9 +77,9 @@ class RedisStoreService extends index_1.StoreService {
77
77
  try {
78
78
  return await (multi || this.redisClient).XADD(key, id, { [args[0]]: args[1] });
79
79
  }
80
- catch (err) {
81
- this.logger.error(`Error publishing 'xadd'; key: ${key}`, err);
82
- throw err;
80
+ catch (error) {
81
+ this.logger.error(`Error publishing 'xadd'; key: ${key}`, { error });
82
+ throw error;
83
83
  }
84
84
  }
85
85
  async xpending(key, group, start, end, count, consumer) {
@@ -95,18 +95,18 @@ class RedisStoreService extends index_1.StoreService {
95
95
  args.push(consumer);
96
96
  return await this.redisClient.sendCommand(['XPENDING', ...args]);
97
97
  }
98
- catch (err) {
99
- this.logger.error(`Error in retrieving pending messages for group: ${group} in key: ${key}`, err);
100
- throw err;
98
+ catch (error) {
99
+ this.logger.error(`Error in retrieving pending messages for group: ${group} in key: ${key}`, { error });
100
+ throw error;
101
101
  }
102
102
  }
103
103
  async xclaim(key, group, consumer, minIdleTime, id, ...args) {
104
104
  try {
105
105
  return await this.redisClient.sendCommand(['XCLAIM', key, group, consumer, minIdleTime.toString(), id, ...args]);
106
106
  }
107
- catch (err) {
108
- this.logger.error(`Error in claiming message with id: ${id} in group: ${group} for key: ${key}`, err);
109
- throw err;
107
+ catch (error) {
108
+ this.logger.error(`Error in claiming message with id: ${id} in group: ${group} for key: ${key}`, { error });
109
+ throw error;
110
110
  }
111
111
  }
112
112
  async xack(key, group, id, multi) {
@@ -119,9 +119,9 @@ class RedisStoreService extends index_1.StoreService {
119
119
  return await this.redisClient[this.commands.xack](key, group, id);
120
120
  }
121
121
  }
122
- catch (err) {
123
- this.logger.error(`Error in acknowledging messages in group: ${group} for key: ${key}`, err);
124
- throw err;
122
+ catch (error) {
123
+ this.logger.error(`Error in acknowledging messages in group: ${group} for key: ${key}`, { error });
124
+ throw error;
125
125
  }
126
126
  }
127
127
  async xdel(key, id, multi) {
@@ -134,9 +134,9 @@ class RedisStoreService extends index_1.StoreService {
134
134
  return await this.redisClient[this.commands.xdel](key, id);
135
135
  }
136
136
  }
137
- catch (err) {
138
- this.logger.error(`Error in deleting messages with ids: ${id} for key: ${key}`, err);
139
- throw err;
137
+ catch (error) {
138
+ this.logger.error(`Error in deleting messages with ids: ${id} for key: ${key}`, { error });
139
+ throw error;
140
140
  }
141
141
  }
142
142
  }
@@ -78,7 +78,7 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
78
78
  deleteHookSignal(topic: string, resolved: string): Promise<number | undefined>;
79
79
  addTaskQueues(keys: string[]): Promise<void>;
80
80
  getActiveTaskQueue(): Promise<string | null>;
81
- deleteProcessedTaskQueue(workItemKey: string, key: string, processedKey: string): Promise<void>;
81
+ deleteProcessedTaskQueue(workItemKey: string, key: string, processedKey: string, scrub?: boolean): Promise<void>;
82
82
  processTaskQueue(sourceKey: string, destinationKey: string): Promise<any>;
83
83
  expireJob(jobId: string, inSeconds: number): Promise<void>;
84
84
  registerTimeHook(jobId: string, activityId: string, type: 'sleep' | 'expire' | 'cron', deletionTime: number, multi?: U): Promise<void>;
@@ -355,7 +355,8 @@ class StoreService {
355
355
  const output = {};
356
356
  for (const [index, result] of results.entries()) {
357
357
  const key = indexKeys[index];
358
- const idsList = result[1];
358
+ //todo: resolve this discrepancy between redis/ioredis
359
+ const idsList = result[1] || result;
359
360
  if (idsList && idsList.length > 0) {
360
361
  output[key] = idsList;
361
362
  }
@@ -601,11 +602,18 @@ class StoreService {
601
602
  }
602
603
  return workItemKey;
603
604
  }
604
- async deleteProcessedTaskQueue(workItemKey, key, processedKey) {
605
+ async deleteProcessedTaskQueue(workItemKey, key, processedKey, scrub = false) {
605
606
  const zsetKey = this.mintKey(key_1.KeyType.WORK_ITEMS, { appId: this.appId });
606
607
  const didRemove = await this.redisClient[this.commands.zrem](zsetKey, workItemKey);
607
608
  if (didRemove) {
608
- await this.redisClient[this.commands.rename](processedKey, key);
609
+ if (scrub) {
610
+ //indexes can be designed to be self-cleaning; `engine.hookAll` exposes this option
611
+ this.redisClient[this.commands.expire](processedKey, 0);
612
+ this.redisClient[this.commands.expire](key.split(":").slice(0, 5).join(":"), 0);
613
+ }
614
+ else {
615
+ await this.redisClient[this.commands.rename](processedKey, key);
616
+ }
609
617
  }
610
618
  this.cache.removeWorkItem(this.appId);
611
619
  }
@@ -15,16 +15,16 @@ class TaskService {
15
15
  async processWebHooks(hookEventCallback) {
16
16
  const workItemKey = await this.store.getActiveTaskQueue();
17
17
  if (workItemKey) {
18
- const [topic, sourceKey, ...sdata] = workItemKey.split('::');
18
+ const [topic, sourceKey, scrub, ...sdata] = workItemKey.split('::');
19
19
  const data = JSON.parse(sdata.join('::'));
20
20
  const destinationKey = `${sourceKey}:processed`;
21
21
  const jobId = await this.store.processTaskQueue(sourceKey, destinationKey);
22
22
  if (jobId) {
23
+ //todo: don't use 'id', make configurable using hook rule
23
24
  await hookEventCallback(topic, { ...data, id: jobId });
24
- //todo: do final checksum count (values are tracked in the stats hash)
25
25
  }
26
26
  else {
27
- await this.store.deleteProcessedTaskQueue(workItemKey, sourceKey, destinationKey);
27
+ await this.store.deleteProcessedTaskQueue(workItemKey, sourceKey, destinationKey, scrub === 'true');
28
28
  }
29
29
  setImmediate(() => this.processWebHooks(hookEventCallback));
30
30
  }
@@ -1,6 +1,6 @@
1
1
  import { MetricTypes } from "./stats";
2
2
  import { StreamRetryPolicy } from "./stream";
3
- type ActivityExecutionType = 'trigger' | 'await' | 'worker' | 'activity' | 'emit' | 'iterate' | 'cycle';
3
+ type ActivityExecutionType = 'trigger' | 'await' | 'worker' | 'activity' | 'emit' | 'iterate' | 'cycle' | 'signal';
4
4
  type Consumes = Record<string, string[]>;
5
5
  interface BaseActivity {
6
6
  title?: string;
@@ -12,6 +12,7 @@ interface BaseActivity {
12
12
  job?: Record<string, any>;
13
13
  hook?: Record<string, any>;
14
14
  telemetry?: Record<string, any>;
15
+ emit?: boolean;
15
16
  sleep?: number;
16
17
  expire?: number;
17
18
  retry?: StreamRetryPolicy;
@@ -37,6 +38,7 @@ interface TriggerActivityStats {
37
38
  key?: {
38
39
  [key: string]: unknown;
39
40
  } | string;
41
+ granularity?: string;
40
42
  measures?: Measure[];
41
43
  }
42
44
  interface TriggerActivity extends BaseActivity {
@@ -53,17 +55,24 @@ interface WorkerActivity extends BaseActivity {
53
55
  topic: string;
54
56
  timeout: number;
55
57
  }
56
- interface EmitActivity extends BaseActivity {
57
- type: 'emit';
58
- }
59
58
  interface CycleActivity extends BaseActivity {
60
59
  type: 'cycle';
61
60
  ancestor: string;
62
61
  }
62
+ interface SignalActivity extends BaseActivity {
63
+ type: 'signal';
64
+ subtype: 'one' | 'all';
65
+ topic: string;
66
+ key_name: string;
67
+ key_value: string;
68
+ scrub: boolean;
69
+ signal?: Record<string, any>;
70
+ resolver?: Record<string, any>;
71
+ }
63
72
  interface IterateActivity extends BaseActivity {
64
73
  type: 'iterate';
65
74
  }
66
- type ActivityType = BaseActivity | TriggerActivity | AwaitActivity | WorkerActivity | EmitActivity | IterateActivity;
75
+ type ActivityType = BaseActivity | TriggerActivity | AwaitActivity | WorkerActivity | IterateActivity;
67
76
  type ActivityData = Record<string, any>;
68
77
  type ActivityMetadata = {
69
78
  aid: string;
@@ -89,4 +98,4 @@ type ActivityDataType = {
89
98
  hook?: Record<string, unknown>;
90
99
  };
91
100
  type ActivityLeg = 1 | 2;
92
- export { ActivityContext, ActivityData, ActivityDataType, ActivityDuplex, ActivityLeg, ActivityMetadata, ActivityType, Consumes, TriggerActivityStats, AwaitActivity, CycleActivity, BaseActivity, EmitActivity, IterateActivity, TriggerActivity, WorkerActivity };
101
+ export { ActivityContext, ActivityData, ActivityDataType, ActivityDuplex, ActivityLeg, ActivityMetadata, ActivityType, Consumes, TriggerActivityStats, AwaitActivity, CycleActivity, SignalActivity, BaseActivity, IterateActivity, TriggerActivity, WorkerActivity };
@@ -1,4 +1,10 @@
1
1
  import { RedisClass, RedisOptions } from './redis';
2
+ type WorkflowConfig = {
3
+ backoffCoefficient?: number;
4
+ maximumAttempts?: number;
5
+ maximumInterval?: string;
6
+ initialInterval?: string;
7
+ };
2
8
  type WorkflowOptions = {
3
9
  taskQueue: string;
4
10
  args: any[];
@@ -6,6 +12,13 @@ type WorkflowOptions = {
6
12
  workflowName?: string;
7
13
  workflowTrace?: string;
8
14
  workflowSpan?: string;
15
+ config?: WorkflowConfig;
16
+ };
17
+ type SignalOptions = {
18
+ taskQueue: string;
19
+ data: Record<string, any>;
20
+ workflowId: string;
21
+ workflowName?: string;
9
22
  };
10
23
  type ActivityWorkflowDataType = {
11
24
  activityName: string;
@@ -39,7 +52,7 @@ type WorkerConfig = {
39
52
  };
40
53
  type WorkerOptions = {
41
54
  maxSystemRetries?: number;
42
- backoffExponent?: number;
55
+ backoffCoefficient?: number;
43
56
  };
44
57
  type ContextType = {
45
58
  workflowId: string;
@@ -59,4 +72,4 @@ type ActivityConfig = {
59
72
  maximumInterval: string;
60
73
  };
61
74
  };
62
- export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection, NativeConnection, ProxyType, Registry, WorkerConfig, WorkerOptions, WorkflowDataType, WorkflowOptions, };
75
+ export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection, NativeConnection, ProxyType, Registry, SignalOptions, WorkerConfig, WorkflowConfig, WorkerOptions, WorkflowDataType, WorkflowOptions, };
@@ -1,9 +1,9 @@
1
- export { ActivityType, ActivityDataType, ActivityContext, ActivityData, ActivityDuplex, ActivityLeg, ActivityMetadata, Consumes, AwaitActivity, BaseActivity, CycleActivity, EmitActivity, WorkerActivity, IterateActivity, TriggerActivity, TriggerActivityStats } from './activity';
1
+ export { ActivityType, ActivityDataType, ActivityContext, ActivityData, ActivityDuplex, ActivityLeg, ActivityMetadata, Consumes, AwaitActivity, BaseActivity, CycleActivity, WorkerActivity, IterateActivity, SignalActivity, TriggerActivity, TriggerActivityStats } from './activity';
2
2
  export { App, AppVID, AppTransitions, AppSubscriptions } from './app';
3
3
  export { AsyncSignal } from './async';
4
4
  export { CacheMode } from './cache';
5
5
  export { CollationFaultType, CollationStage } from './collator';
6
- export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection, NativeConnection, ProxyType, Registry, WorkerConfig, WorkerOptions, WorkflowDataType, WorkflowOptions, } from './durable';
6
+ export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection, NativeConnection, ProxyType, Registry, WorkflowConfig, WorkerConfig, WorkerOptions, WorkflowDataType, WorkflowOptions, } from './durable';
7
7
  export { HookCondition, HookConditions, HookGate, HookInterface, HookRule, HookRules, HookSignal } from './hook';
8
8
  export { RedisClientType as IORedisClientType, RedisMultiType as IORedisMultiType } from './ioredisclient';
9
9
  export { ILogger } from './logger';
@@ -32,6 +32,7 @@ interface JobStatsInput {
32
32
  start?: string;
33
33
  end?: string;
34
34
  sparse?: boolean;
35
+ scrub?: boolean;
35
36
  }
36
37
  interface GetStatsOptions {
37
38
  key: string;
package/modules/errors.ts CHANGED
@@ -12,6 +12,38 @@ class SetStateError extends Error {
12
12
  }
13
13
  }
14
14
 
15
+ //thrown when a signal set is incomplete but already configured
16
+ //if a waitFor set has 'n' items, this can be thrown `n - 1` times
17
+ class DurableIncompleteSignalError extends Error {
18
+ code: number;
19
+ constructor(message: string) {
20
+ super(message);
21
+ this.code = 593;
22
+ }
23
+ }
24
+
25
+ //the original waitFor error that is thrown for a new signal set
26
+ class DurableWaitForSignalError extends Error {
27
+ code: number;
28
+ signals: {signal: string, index: number}[]; //signal id and execution order in the workflow
29
+ constructor(message: string, signals: {signal: string, index: number}[]) {
30
+ super(message);
31
+ this.signals = signals;
32
+ this.code = 594;
33
+ }
34
+ }
35
+
36
+ class DurableSleepError extends Error {
37
+ code: number;
38
+ duration: number; //seconds
39
+ index: number; //execution order in the workflow
40
+ constructor(message: string, duration: number, index: number) {
41
+ super(message);
42
+ this.duration = duration;
43
+ this.index = index;
44
+ this.code = 595;
45
+ }
46
+ }
15
47
  class DurableTimeoutError extends Error {
16
48
  code: number;
17
49
  constructor(message: string) {
@@ -87,6 +119,9 @@ export {
87
119
  DurableMaxedError,
88
120
  DurableFatalError,
89
121
  DurableRetryError,
122
+ DurableWaitForSignalError,
123
+ DurableIncompleteSignalError,
124
+ DurableSleepError,
90
125
  DuplicateJobError,
91
126
  GetStateError,
92
127
  SetStateError,
package/modules/utils.ts CHANGED
@@ -86,9 +86,13 @@ export async function getSubscriptionTopic(activityId: string, store: StoreServi
86
86
  }
87
87
 
88
88
  /**
89
- * returns the 12-digit format of the iso timestamp (e.g, 202101010000)
89
+ * returns the 12-digit format of the iso timestamp (e.g, 202101010000); returns
90
+ * an empty string if overridden by the user to not segment by time (infinity).
90
91
  */
91
92
  export function getTimeSeries(granularity: string): string {
93
+ if (granularity.toString() === 'infinity') {
94
+ return '0';
95
+ }
92
96
  const now = new Date();
93
97
  const granularityUnit = granularity.slice(-1);
94
98
  const granularityValue = parseInt(granularity.slice(0, -1), 10);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.12",
4
- "description": "Durable Workflows",
3
+ "version": "0.0.14",
4
+ "description": "Unbreakable Workflows",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
7
7
  "repository": {
@@ -26,6 +26,9 @@
26
26
  "test:connect": "NODE_ENV=test jest ./tests/unit/services/connector/index.test.ts --detectOpenHandles --forceExit --verbose",
27
27
  "test:connect:redis": "NODE_ENV=test jest ./tests/unit/services/connector/clients/redis.test.ts --detectOpenHandles --forceExit --verbose",
28
28
  "test:connect:ioredis": "NODE_ENV=test jest ./tests/unit/services/connector/clients/ioredis.test.ts --detectOpenHandles --forceExit --verbose",
29
+ "test:emit": "NODE_ENV=test jest ./tests/functional/emit/index.test.ts --detectOpenHandles --forceExit --verbose",
30
+ "test:hook": "NODE_ENV=test jest ./tests/functional/hook/index.test.ts --detectOpenHandles --forceExit --verbose",
31
+ "test:signal": "NODE_ENV=test jest ./tests/functional/signal/index.test.ts --detectOpenHandles --forceExit --verbose",
29
32
  "test:parallel": "NODE_ENV=test jest ./tests/functional/parallel/index.test.ts --detectOpenHandles --forceExit --verbose",
30
33
  "test:sequence": "NODE_ENV=test jest ./tests/functional/sequence/index.test.ts --detectOpenHandles --forceExit --verbose",
31
34
  "test:quorum": "NODE_ENV=test jest ./tests/functional/quorum/index.test.ts --detectOpenHandles --forceExit --verbose",
@@ -44,6 +47,8 @@
44
47
  "test:durable:goodbye": "NODE_ENV=test jest ./tests/durable/goodbye/index.test.ts --detectOpenHandles --forceExit --verbose",
45
48
  "test:durable:retry": "NODE_ENV=test jest ./tests/durable/retry/index.test.ts --detectOpenHandles --forceExit --verbose",
46
49
  "test:durable:fatal": "NODE_ENV=test jest ./tests/durable/fatal/index.test.ts --detectOpenHandles --forceExit --verbose",
50
+ "test:durable:sleep": "NODE_ENV=test jest ./tests/durable/sleep/index.test.ts --detectOpenHandles --forceExit --verbose",
51
+ "test:durable:signal": "NODE_ENV=test jest ./tests/durable/signal/index.test.ts --detectOpenHandles --forceExit --verbose",
47
52
  "test:durable:loopactivity": "NODE_ENV=test jest ./tests/durable/loopactivity/index.test.ts --detectOpenHandles --forceExit --verbose",
48
53
  "test:durable:nested": "NODE_ENV=test jest ./tests/durable/nested/index.test.ts --detectOpenHandles --forceExit --verbose"
49
54
  },
@@ -30,6 +30,7 @@ import {
30
30
  StreamDataType,
31
31
  StreamStatus } from '../../types/stream';
32
32
  import { TransitionRule } from '../../types/transition';
33
+ import { HookRule } from '../../types/hook';
33
34
 
34
35
  /**
35
36
  * The base class for all activities
@@ -114,9 +115,9 @@ class Activity {
114
115
  return this.context.metadata.aid;
115
116
  } catch (error) {
116
117
  if (error instanceof GetStateError) {
117
- this.logger.error('activity-get-state-error', error);
118
+ this.logger.error('activity-get-state-error', { error });
118
119
  } else {
119
- this.logger.error('activity-process-error', error);
120
+ this.logger.error('activity-process-error', { error });
120
121
  }
121
122
  telemetry.setActivityError(error.message);
122
123
  throw error;
@@ -135,6 +136,11 @@ class Activity {
135
136
  return !!(this.config.hook?.topic || this.config.sleep);
136
137
  }
137
138
 
139
+ async getHookRule(topic: string): Promise<HookRule | undefined> {
140
+ const rules = await this.store.getHookRules();
141
+ return rules?.[topic]?.[0] as HookRule;
142
+ }
143
+
138
144
  async registerHook(multi?: RedisMulti): Promise<string | void> {
139
145
  if (this.config.hook?.topic) {
140
146
  const signaler = new StoreSignaler(this.store, this.logger);
@@ -203,7 +209,7 @@ class Activity {
203
209
  telemetry.setActivityAttributes(attrs);
204
210
  return jobStatus as number;
205
211
  } catch (error) {
206
- this.logger.error('engine-process-hook-event-error', error);
212
+ this.logger.error('engine-process-hook-event-error', { error });
207
213
  telemetry.setActivityError(error.message);
208
214
  throw error;
209
215
  } finally {
@@ -245,11 +251,11 @@ class Activity {
245
251
  }
246
252
  this.transitionAdjacent(multiResponse, telemetry);
247
253
  } catch (error) {
248
- this.logger.error('activity-process-event-error', error);
249
- telemetry.setActivityError(error.message);
254
+ this.logger.error('activity-process-event-error', { error });
255
+ telemetry && telemetry.setActivityError(error.message);
250
256
  throw error;
251
257
  } finally {
252
- telemetry.endActivitySpan();
258
+ telemetry && telemetry.endActivitySpan();
253
259
  this.logger.debug('activity-process-event-end', { jid, aid });
254
260
  }
255
261
  }
@@ -570,14 +576,18 @@ class Activity {
570
576
 
571
577
  async transition(adjacencyList: StreamData[], jobStatus: JobStatus): Promise<string[]> {
572
578
  let mIds: string[] = [];
573
- if (adjacencyList.length) {
579
+ if (jobStatus <= 0 || this.config.emit) {
580
+ //activity should not send 'emit' if the job is truly over
581
+ const isTrueEmit = jobStatus > 0;
582
+ await this.engine.runJobCompletionTasks(this.context, isTrueEmit);
583
+ }
584
+
585
+ if (adjacencyList.length && jobStatus > 0) {
574
586
  const multi = this.store.getMulti();
575
587
  for (const execSignal of adjacencyList) {
576
588
  await this.engine.streamSignaler?.publishMessage(null, execSignal, multi);
577
589
  }
578
590
  mIds = (await multi.exec()) as string[];
579
- } else if (jobStatus <= 0) {
580
- await this.engine.runJobCompletionTasks(this.context);
581
591
  }
582
592
  return mIds;
583
593
  }