@hotmeshio/hotmesh 0.1.3 → 0.1.5

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 (41) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  3. package/README.md +15 -12
  4. package/build/modules/enums.d.ts +1 -0
  5. package/build/modules/enums.js +2 -1
  6. package/build/modules/key.d.ts +1 -2
  7. package/build/modules/key.js +3 -4
  8. package/build/modules/utils.js +10 -11
  9. package/build/package.json +4 -3
  10. package/build/services/activities/trigger.d.ts +2 -1
  11. package/build/services/activities/trigger.js +20 -8
  12. package/build/services/compiler/deployer.js +1 -1
  13. package/build/services/durable/client.js +5 -1
  14. package/build/services/durable/worker.js +1 -1
  15. package/build/services/engine/index.d.ts +1 -1
  16. package/build/services/engine/index.js +13 -6
  17. package/build/services/hotmesh/index.js +14 -2
  18. package/build/services/router/index.d.ts +2 -0
  19. package/build/services/router/index.js +37 -15
  20. package/build/services/store/clients/ioredis.js +28 -8
  21. package/build/services/store/clients/redis.js +27 -10
  22. package/build/services/store/index.d.ts +6 -2
  23. package/build/services/store/index.js +38 -5
  24. package/build/services/stream/clients/ioredis.js +1 -1
  25. package/build/services/stream/clients/redis.js +23 -23
  26. package/build/services/worker/index.d.ts +1 -1
  27. package/build/services/worker/index.js +4 -2
  28. package/build/types/durable.d.ts +11 -0
  29. package/build/types/hotmesh.d.ts +1 -1
  30. package/build/types/hotmesh.js +1 -1
  31. package/build/types/job.d.ts +11 -0
  32. package/build/types/quorum.d.ts +2 -4
  33. package/build/types/redis.d.ts +3 -0
  34. package/build/types/stream.d.ts +2 -0
  35. package/package.json +4 -3
  36. package/types/durable.ts +14 -1
  37. package/types/hotmesh.ts +1 -1
  38. package/types/job.ts +12 -0
  39. package/types/quorum.ts +2 -4
  40. package/types/redis.ts +3 -0
  41. package/types/stream.ts +2 -0
@@ -29,17 +29,17 @@ class IORedisStoreService extends index_1.StoreService {
29
29
  async exec() {
30
30
  if (commands.length === 0)
31
31
  return [];
32
- const sameKey = commands.every(cmd => {
32
+ const sameKey = commands.every((cmd) => {
33
33
  return cmd.args[0] === commands[0].args[0];
34
34
  });
35
35
  if (sameKey) {
36
36
  const multi = my.redisClient.multi();
37
- commands.forEach(cmd => multi[cmd.command](...cmd.args));
37
+ commands.forEach((cmd) => multi[cmd.command](...cmd.args));
38
38
  const results = await multi.exec();
39
- return results.map(item => item);
39
+ return results.map((item) => item);
40
40
  }
41
41
  else {
42
- return Promise.all(commands.map(cmd => my.redisClient[cmd.command](...cmd.args)));
42
+ return Promise.all(commands.map((cmd) => my.redisClient[cmd.command](...cmd.args)));
43
43
  }
44
44
  },
45
45
  xadd(key, id, fields, message) {
@@ -55,10 +55,30 @@ class IORedisStoreService extends index_1.StoreService {
55
55
  return addCommand('xlen', [key]);
56
56
  },
57
57
  xpending(key, group, start, end, count, consumer) {
58
- return addCommand('xpending', [key, group, start, end, count, consumer]);
58
+ return addCommand('xpending', [
59
+ key,
60
+ group,
61
+ start,
62
+ end,
63
+ count,
64
+ consumer,
65
+ ]);
59
66
  },
60
67
  xclaim(key, group, consumer, minIdleTime, id, ...args) {
61
- return addCommand('xclaim', [key, group, consumer, minIdleTime, id, ...args]);
68
+ return addCommand('xclaim', [
69
+ key,
70
+ group,
71
+ consumer,
72
+ minIdleTime,
73
+ id,
74
+ ...args,
75
+ ]);
76
+ },
77
+ del(key) {
78
+ return addCommand('del', [key]);
79
+ },
80
+ expire: function (key, seconds) {
81
+ return addCommand('expire', [key, seconds]);
62
82
  },
63
83
  hdel(key, itemId) {
64
84
  return addCommand('hdel', [key, itemId]);
@@ -89,7 +109,7 @@ class IORedisStoreService extends index_1.StoreService {
89
109
  },
90
110
  xgroup(command, key, groupName, id, mkStream) {
91
111
  return addCommand('xgroup', [command, key, groupName, id, mkStream]);
92
- }
112
+ },
93
113
  };
94
114
  return multiInstance;
95
115
  }
@@ -131,7 +151,7 @@ class IORedisStoreService extends index_1.StoreService {
131
151
  return ((await this.redisClient.xgroup(command, key, groupName, id, mkStream)) === 'OK');
132
152
  }
133
153
  catch (err) {
134
- this.logger.debug(`Consumer group not created with MKSTREAM for key: ${key} and group: ${groupName}`);
154
+ this.logger.debug('stream-mkstream-caught', { key, group: groupName });
135
155
  throw err;
136
156
  }
137
157
  }
@@ -58,22 +58,22 @@ class RedisStoreService extends index_1.StoreService {
58
58
  async exec() {
59
59
  if (commands.length === 0)
60
60
  return [];
61
- const sameKey = commands.every(cmd => {
61
+ const sameKey = commands.every((cmd) => {
62
62
  return cmd.args[0] === commands[0].args[0];
63
63
  });
64
64
  if (sameKey) {
65
65
  const multi = my.redisClient.multi();
66
- commands.forEach(cmd => {
66
+ commands.forEach((cmd) => {
67
67
  if (cmd.command === 'ZADD') {
68
68
  return multi.ZADD(cmd.args[0], cmd.args[1], cmd.args[2]);
69
69
  }
70
70
  return multi[cmd.command](...cmd.args);
71
71
  });
72
72
  const results = await multi.exec();
73
- return results.map(item => item);
73
+ return results.map((item) => item);
74
74
  }
75
75
  else {
76
- return Promise.all(commands.map(cmd => {
76
+ return Promise.all(commands.map((cmd) => {
77
77
  if (cmd.command === 'ZADD') {
78
78
  return my.redisClient.ZADD(cmd.args[0], cmd.args[1], cmd.args[2]);
79
79
  }
@@ -94,10 +94,30 @@ class RedisStoreService extends index_1.StoreService {
94
94
  return addCommand('XLEN', [key]);
95
95
  },
96
96
  XCLAIM(key, group, consumer, minIdleTime, id, ...args) {
97
- return addCommand('XCLAIM', [key, group, consumer, minIdleTime, id, ...args]);
97
+ return addCommand('XCLAIM', [
98
+ key,
99
+ group,
100
+ consumer,
101
+ minIdleTime,
102
+ id,
103
+ ...args,
104
+ ]);
98
105
  },
99
106
  XPENDING(key, group, start, end, count, consumer) {
100
- return addCommand('XPENDING', [key, group, start, end, count, consumer]);
107
+ return addCommand('XPENDING', [
108
+ key,
109
+ group,
110
+ start,
111
+ end,
112
+ count,
113
+ consumer,
114
+ ]);
115
+ },
116
+ DEL: function (key) {
117
+ return addCommand('DEL', [key]);
118
+ },
119
+ EXPIRE: function (key, seconds) {
120
+ return addCommand('EXPIRE', [key, seconds]);
101
121
  },
102
122
  HDEL(key, itemId) {
103
123
  return addCommand('HDEL', [key, itemId]);
@@ -129,9 +149,6 @@ class RedisStoreService extends index_1.StoreService {
129
149
  XGROUP(command, key, groupName, id, mkStream) {
130
150
  return addCommand('XGROUP', [command, key, groupName, id, mkStream]);
131
151
  },
132
- DEL: function (key) {
133
- throw new Error('Function not implemented.');
134
- },
135
152
  EXISTS: function (key) {
136
153
  throw new Error('Function not implemented.');
137
154
  },
@@ -152,7 +169,7 @@ class RedisStoreService extends index_1.StoreService {
152
169
  },
153
170
  ZSCORE: function (key, value) {
154
171
  throw new Error('Function not implemented.');
155
- }
172
+ },
156
173
  };
157
174
  return multiInstance;
158
175
  }
@@ -11,6 +11,7 @@ import { Transitions } from '../../types/transition';
11
11
  import { ReclaimedMessageType } from '../../types/stream';
12
12
  import { JobCompletionOptions, JobInterruptOptions } from '../../types/job';
13
13
  import { WorkListTaskType } from '../../types/task';
14
+ import { ThrottleOptions } from '../../types/quorum';
14
15
  import { Cache } from './cache';
15
16
  interface AbstractRedisClient {
16
17
  exec(): any;
@@ -101,7 +102,7 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
101
102
  * purposeful re-entry.
102
103
  */
103
104
  collateSynthetic(jobId: string, guid: string, amount: number, multi?: U): Promise<number>;
104
- setStateNX(jobId: string, appId: string): Promise<boolean>;
105
+ setStateNX(jobId: string, appId: string, status?: number): Promise<boolean>;
105
106
  getSchema(activityId: string, appVersion: AppVID): Promise<ActivityType>;
106
107
  getSchemas(appVersion: AppVID): Promise<Record<string, ActivityType>>;
107
108
  setSchemas(schemas: Record<string, ActivityType>, appVersion: AppVID): Promise<any>;
@@ -119,7 +120,7 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
119
120
  getActiveTaskQueue(): Promise<string | null>;
120
121
  deleteProcessedTaskQueue(workItemKey: string, key: string, processedKey: string, scrub?: boolean): Promise<void>;
121
122
  processTaskQueue(sourceKey: string, destinationKey: string): Promise<any>;
122
- expireJob(jobId: string, inSeconds: number): Promise<void>;
123
+ expireJob(jobId: string, inSeconds: number, redisMulti?: U): Promise<void>;
123
124
  /**
124
125
  * register the descendants of an expired origin flow to be
125
126
  * expired at a future date; options indicate whether this
@@ -162,5 +163,8 @@ declare abstract class StoreService<T, U extends AbstractRedisClient> {
162
163
  scrub(jobId: string): Promise<void>;
163
164
  findJobs(queryString?: string, limit?: number, batchSize?: number, cursor?: string): Promise<[string, string[]]>;
164
165
  findJobFields(jobId: string, fieldMatchPattern?: string, limit?: number, batchSize?: number, cursor?: string): Promise<[string, StringStringType]>;
166
+ setThrottleRate(options: ThrottleOptions): Promise<void>;
167
+ getThrottleRates(): Promise<StringStringType>;
168
+ getThrottleRate(topic: string): Promise<number>;
165
169
  }
166
170
  export { StoreService };
@@ -24,11 +24,11 @@ var __importStar = (this && this.__importStar) || function (mod) {
24
24
  };
25
25
  Object.defineProperty(exports, "__esModule", { value: true });
26
26
  exports.StoreService = void 0;
27
+ const errors_1 = require("../../modules/errors");
27
28
  const key_1 = require("../../modules/key");
28
29
  const serializer_1 = require("../serializer");
29
30
  const utils_1 = require("../../modules/utils");
30
31
  const enums_1 = require("../../modules/enums");
31
- const errors_1 = require("../../modules/errors");
32
32
  const cache_1 = require("./cache");
33
33
  class StoreService {
34
34
  constructor(redisClient) {
@@ -593,9 +593,9 @@ class StoreService {
593
593
  });
594
594
  return await (multi || this.redisClient)[this.commands.hincrbyfloat](jobKey, guid, amount);
595
595
  }
596
- async setStateNX(jobId, appId) {
596
+ async setStateNX(jobId, appId, status) {
597
597
  const hashKey = this.mintKey(key_1.KeyType.JOB_STATE, { appId, jobId });
598
- const result = await this.redisClient[this.commands.hsetnx](hashKey, ':', '1');
598
+ const result = await this.redisClient[this.commands.hsetnx](hashKey, ':', status?.toString() ?? '1');
599
599
  return this.isSuccessful(result);
600
600
  }
601
601
  async getSchema(activityId, appVersion) {
@@ -803,13 +803,13 @@ class StoreService {
803
803
  async processTaskQueue(sourceKey, destinationKey) {
804
804
  return await this.redisClient[this.commands.lmove](sourceKey, destinationKey, 'LEFT', 'RIGHT');
805
805
  }
806
- async expireJob(jobId, inSeconds) {
806
+ async expireJob(jobId, inSeconds, redisMulti) {
807
807
  if (!isNaN(inSeconds) && inSeconds > 0) {
808
808
  const jobKey = this.mintKey(key_1.KeyType.JOB_STATE, {
809
809
  appId: this.appId,
810
810
  jobId,
811
811
  });
812
- await this.redisClient[this.commands.expire](jobKey, inSeconds);
812
+ await (redisMulti || this.redisClient)[this.commands.expire](jobKey, inSeconds);
813
813
  }
814
814
  }
815
815
  /**
@@ -1004,5 +1004,38 @@ class StoreService {
1004
1004
  } while (cursor !== '0' && len < limit);
1005
1005
  return [cursor, matchingFields];
1006
1006
  }
1007
+ async setThrottleRate(options) {
1008
+ const key = this.mintKey(key_1.KeyType.THROTTLE_RATE, { appId: this.appId });
1009
+ //engine guids are session specific. no need to persist
1010
+ if (options.guid) {
1011
+ return;
1012
+ }
1013
+ //if a topic, update one
1014
+ const rate = options.throttle.toString();
1015
+ if (options.topic) {
1016
+ await this.redisClient[this.commands.hset](key, { [options.topic]: rate });
1017
+ }
1018
+ else {
1019
+ //if no topic, update all
1020
+ const multi = this.getMulti();
1021
+ multi[this.commands.del](key);
1022
+ multi[this.commands.hset](key, { ':': rate });
1023
+ await multi.exec();
1024
+ }
1025
+ }
1026
+ async getThrottleRates() {
1027
+ const key = this.mintKey(key_1.KeyType.THROTTLE_RATE, { appId: this.appId });
1028
+ const response = await this.redisClient[this.commands.hgetall](key);
1029
+ return response ?? {};
1030
+ }
1031
+ async getThrottleRate(topic) {
1032
+ const response = await this.getThrottleRates();
1033
+ const rate = topic in response ? Number(response[topic]) : 0;
1034
+ if (isNaN(rate))
1035
+ return 0;
1036
+ if (rate == -1)
1037
+ return enums_1.MAX_DELAY;
1038
+ return Math.max(Math.min(rate, enums_1.MAX_DELAY), 0);
1039
+ }
1007
1040
  }
1008
1041
  exports.StoreService = StoreService;
@@ -26,7 +26,7 @@ class IORedisStreamService extends index_1.StreamService {
26
26
  return ((await this.redisClient.xgroup(command, key, groupName, id, mkStream)) === 'OK');
27
27
  }
28
28
  catch (error) {
29
- this.logger.info(`Consumer group not created with MKSTREAM for key: ${key} and group: ${groupName}`);
29
+ this.logger.debug('stream-mkstream-caught', { key, group: groupName });
30
30
  throw error;
31
31
  }
32
32
  }
@@ -32,10 +32,10 @@ class RedisStreamService extends index_1.StreamService {
32
32
  ...args,
33
33
  ])) === 1);
34
34
  }
35
- catch (err) {
35
+ catch (error) {
36
36
  const streamType = mkStream === 'MKSTREAM' ? 'with MKSTREAM' : 'without MKSTREAM';
37
- this.logger.error(`x-group-error ${streamType} for key: ${key} and group: ${groupName}`, err);
38
- throw err;
37
+ this.logger.error(`x-group-error ${streamType} for key: ${key} and group: ${groupName}`, { ...error });
38
+ throw error;
39
39
  }
40
40
  }
41
41
  async xadd(key, id, ...args) {
@@ -48,9 +48,9 @@ class RedisStreamService extends index_1.StreamService {
48
48
  [args[0]]: args[1],
49
49
  });
50
50
  }
51
- catch (err) {
52
- this.logger.error(`Error publishing 'xadd'; key: ${key}`, err);
53
- throw err;
51
+ catch (error) {
52
+ this.logger.error(`Error publishing 'xadd'; key: ${key}`, { ...error });
53
+ throw error;
54
54
  }
55
55
  }
56
56
  async xreadgroup(command, groupName, consumerName, blockOption, blockTime, streamsOption, streamName, id) {
@@ -67,9 +67,9 @@ class RedisStreamService extends index_1.StreamService {
67
67
  id,
68
68
  ]);
69
69
  }
70
- catch (err) {
71
- this.logger.error(`Error in reading data from group: ${groupName} in stream: ${streamName}`, err);
72
- throw err;
70
+ catch (error) {
71
+ this.logger.error(`Error reading stream data [Stream ${streamName}] [Group ${groupName}]`, { ...error });
72
+ throw error;
73
73
  }
74
74
  }
75
75
  async xpending(key, group, start, end, count, consumer) {
@@ -86,13 +86,13 @@ class RedisStreamService extends index_1.StreamService {
86
86
  try {
87
87
  return await this.redisClient.sendCommand(['XPENDING', ...args]);
88
88
  }
89
- catch (err) {
90
- this.logger.error('err, args', err, args);
89
+ catch (error) {
90
+ this.logger.error('error, args', { ...error }, args);
91
91
  }
92
92
  }
93
- catch (err) {
94
- this.logger.error(`Error in retrieving pending messages for group: ${group} in key: ${key}`, err);
95
- throw err;
93
+ catch (error) {
94
+ this.logger.error(`Error retrieving pending messages for group: ${group} in key: ${key}`, { ...error });
95
+ throw error;
96
96
  }
97
97
  }
98
98
  async xclaim(key, group, consumer, minIdleTime, id, ...args) {
@@ -107,9 +107,9 @@ class RedisStreamService extends index_1.StreamService {
107
107
  ...args,
108
108
  ]));
109
109
  }
110
- catch (err) {
111
- this.logger.error(`Error in claiming message with id: ${id} in group: ${group} for key: ${key}`, err);
112
- throw err;
110
+ catch (error) {
111
+ this.logger.error(`Error claiming message with id: ${id} in group: ${group} for key: ${key}`, { ...error });
112
+ throw error;
113
113
  }
114
114
  }
115
115
  async xack(key, group, id, multi) {
@@ -122,9 +122,9 @@ class RedisStreamService extends index_1.StreamService {
122
122
  return await this.redisClient.XACK(key, group, id);
123
123
  }
124
124
  }
125
- catch (err) {
126
- this.logger.error(`Error in acknowledging messages in group: ${group} for key: ${key}`, err);
127
- throw err;
125
+ catch (error) {
126
+ this.logger.error(`Error acknowledging messages in group: ${group} for key: ${key}`, { ...error });
127
+ throw error;
128
128
  }
129
129
  }
130
130
  async xdel(key, id, multi) {
@@ -137,9 +137,9 @@ class RedisStreamService extends index_1.StreamService {
137
137
  return await this.redisClient.XDEL(key, id);
138
138
  }
139
139
  }
140
- catch (err) {
141
- this.logger.error(`Error in deleting messages with ids: ${id} for key: ${key}`, err);
142
- throw err;
140
+ catch (error) {
141
+ this.logger.error(`Error deleting messages with ids: ${id} for key: ${key}`, { ...error });
142
+ throw error;
143
143
  }
144
144
  }
145
145
  async xlen(key, multi) {
@@ -28,7 +28,7 @@ declare class WorkerService {
28
28
  initStoreChannel(service: WorkerService, store: RedisClient): Promise<void>;
29
29
  initSubChannel(service: WorkerService, sub: RedisClient): Promise<void>;
30
30
  initStreamChannel(service: WorkerService, stream: RedisClient): Promise<void>;
31
- initRouter(worker: HotMeshWorker, logger: ILogger): Router;
31
+ initRouter(worker: HotMeshWorker, logger: ILogger): Promise<Router>;
32
32
  subscriptionHandler(): SubscriptionCallback;
33
33
  /**
34
34
  * A quorum-wide command to broadcaset system details.
@@ -37,7 +37,7 @@ class WorkerService {
37
37
  await service.subscribe.subscribe(key_1.KeyType.QUORUM, service.subscriptionHandler(), appId, service.topic);
38
38
  await service.subscribe.subscribe(key_1.KeyType.QUORUM, service.subscriptionHandler(), appId, service.guid);
39
39
  await service.initStreamChannel(service, worker.stream);
40
- service.router = service.initRouter(worker, logger);
40
+ service.router = await service.initRouter(worker, logger);
41
41
  const key = service.stream.mintKey(key_1.KeyType.STREAMS, {
42
42
  appId: service.appId,
43
43
  topic: worker.topic,
@@ -84,7 +84,8 @@ class WorkerService {
84
84
  }
85
85
  await service.stream.init(service.namespace, service.appId, service.logger);
86
86
  }
87
- initRouter(worker, logger) {
87
+ async initRouter(worker, logger) {
88
+ const throttle = await this.store.getThrottleRate(worker.topic);
88
89
  return new router_1.Router({
89
90
  namespace: this.namespace,
90
91
  appId: this.appId,
@@ -93,6 +94,7 @@ class WorkerService {
93
94
  topic: worker.topic,
94
95
  reclaimDelay: worker.reclaimDelay,
95
96
  reclaimCount: worker.reclaimCount,
97
+ throttle,
96
98
  }, this.stream, this.store, logger);
97
99
  }
98
100
  subscriptionHandler() {
@@ -260,6 +260,17 @@ type WorkflowOptions = {
260
260
  * default is true; if false, will not await the execution
261
261
  */
262
262
  await?: boolean;
263
+ /**
264
+ * If provided, the job will initialize in an expired state, reserving
265
+ * only the job ID (HSETNX) and persisting search and marker (if provided).
266
+ * If a `resume` signal is sent before the specified number of seconds,
267
+ * the job will resume as normal, transition to the adjacent children
268
+ * of the trigger. If the job is not resumed within the number
269
+ * of seconds specified, the job will be scrubbed. No dependencies
270
+ * are added for a job in an expired state; however, dependencies
271
+ * will be added after the job is resumed if relevant.
272
+ */
273
+ expired?: number;
263
274
  };
264
275
  /**
265
276
  * Options for setting up a hook.
@@ -9,7 +9,7 @@ import { LogLevel } from './logger';
9
9
  */
10
10
  declare enum KeyType {
11
11
  APP = "APP",
12
- ENGINE_ID = "ENGINE",
12
+ THROTTLE_RATE = "THROTTLE_RATE",
13
13
  HOOKS = "HOOKS",
14
14
  JOB_DEPENDENTS = "JOB_DEPENDENTS",
15
15
  JOB_STATE = "JOB_STATE",
@@ -7,7 +7,7 @@ exports.KeyType = void 0;
7
7
  var KeyType;
8
8
  (function (KeyType) {
9
9
  KeyType["APP"] = "APP";
10
- KeyType["ENGINE_ID"] = "ENGINE";
10
+ KeyType["THROTTLE_RATE"] = "THROTTLE_RATE";
11
11
  KeyType["HOOKS"] = "HOOKS";
12
12
  KeyType["JOB_DEPENDENTS"] = "JOB_DEPENDENTS";
13
13
  KeyType["JOB_STATE"] = "JOB_STATE";
@@ -77,6 +77,17 @@ type ExtensionType = {
77
77
  * the initial data set.
78
78
  */
79
79
  marker?: StringStringType;
80
+ /**
81
+ * If provided, the job will initialize in an expired state, reserving
82
+ * only the job ID (HSETNX) and persisting search and marker (if provided).
83
+ * If a `resume` signal is sent before the specified number of seconds,
84
+ * the job will resume as normal, transition the the adjacent children
85
+ * of the trigger. If the job is not resumed within the
86
+ * number of seconds specified, the job will be scrubbed. No dependencies
87
+ * are added for a job in an expired state; however, dependencies will be
88
+ * added after the job is resumed if relevant.
89
+ */
90
+ expired?: number;
80
91
  };
81
92
  /**
82
93
  * job_status semaphore
@@ -30,11 +30,9 @@ export type ThrottleOptions = {
30
30
  topic?: string;
31
31
  /** entity/noun */
32
32
  entity?: string;
33
- /** in milliseconds; default is 0 */
33
+ /** in milliseconds; set to -1 for indefinite throttle */
34
34
  throttle: number;
35
- /** namespace
36
- * @default 'durable'
37
- */
35
+ /** namespace */
38
36
  namespace?: string;
39
37
  };
40
38
  export interface QuorumProfile {
@@ -68,6 +68,7 @@ interface RedisRedisMultiType {
68
68
  XLEN(key: string): this;
69
69
  DEL(key: string): this;
70
70
  EXISTS(key: string): this;
71
+ EXPIRE(key: string, seconds: number): this;
71
72
  HDEL(key: string, itemId: string): this;
72
73
  HGET(key: string, itemId: string): this;
73
74
  HGETALL(key: string): this;
@@ -215,6 +216,8 @@ interface IORedisMultiType {
215
216
  xlen(key: string): this;
216
217
  xpending(key: string, group: string, start?: string, end?: string, count?: number, consumer?: string): this;
217
218
  xclaim(key: string, group: string, consumer: string, minIdleTime: number, id: string, ...args: string[]): this;
219
+ del(key: string): this;
220
+ expire(key: string, seconds: number): this;
218
221
  hdel(key: string, itemId: string): this;
219
222
  hget(key: string, itemId: string): this;
220
223
  hgetall(key: string): this;
@@ -122,6 +122,8 @@ export type StreamConfig = {
122
122
  guid: string;
123
123
  /** Role associated with the stream */
124
124
  role: StreamRole;
125
+ /** Default throttle (read from KeyType.THROTTLE_RATE) */
126
+ throttle: number;
125
127
  /** Optional topic for the stream */
126
128
  topic?: string;
127
129
  /** Delay before a message can be reclaimed, defaults to 60,000 milliseconds */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -31,7 +31,7 @@
31
31
  "test:durable:collision": "NODE_ENV=test jest ./tests/durable/collision/index.test.ts --detectOpenHandles --forceExit --verbose",
32
32
  "test:durable:fatal": "NODE_ENV=test jest ./tests/durable/fatal/index.test.ts --detectOpenHandles --forceExit --verbose",
33
33
  "test:durable:goodbye": "NODE_ENV=test jest ./tests/durable/goodbye/index.test.ts --detectOpenHandles --forceExit --verbose",
34
- "test:durable:hello": "NODE_ENV=test jest ./tests/durable/helloworld/index.test.ts --detectOpenHandles --forceExit --verbose",
34
+ "test:durable:hello": "HMSH_IS_CLUSTER=true NODE_ENV=test jest ./tests/durable/helloworld/index.test.ts --detectOpenHandles --forceExit --verbose",
35
35
  "test:durable:hook": "NODE_ENV=test jest ./tests/durable/hook/index.test.ts --detectOpenHandles --forceExit --verbose",
36
36
  "test:durable:interrupt": "NODE_ENV=test jest ./tests/durable/interrupt/index.test.ts --detectOpenHandles --forceExit --verbose",
37
37
  "test:durable:loopactivity": "NODE_ENV=test jest ./tests/durable/loopactivity/index.test.ts --detectOpenHandles --forceExit --verbose",
@@ -41,13 +41,14 @@
41
41
  "test:durable:signal": "NODE_ENV=test jest ./tests/durable/signal/index.test.ts --detectOpenHandles --forceExit --verbose",
42
42
  "test:durable:unknown": "NODE_ENV=test jest ./tests/durable/unknown/index.test.ts --detectOpenHandles --forceExit --verbose",
43
43
  "test:emit": "NODE_ENV=test jest ./tests/functional/emit/index.test.ts --detectOpenHandles --forceExit --verbose",
44
+ "test:expired": "NODE_ENV=test jest ./tests/functional/expired/index.test.ts --detectOpenHandles --forceExit --verbose",
44
45
  "test:functional": "NODE_ENV=test jest ./tests/functional/*/index.test.ts --detectOpenHandles --forceExit --verbose",
45
46
  "test:hmsh": "NODE_ENV=test jest ./tests/functional/index.test.ts --detectOpenHandles --verbose",
46
47
  "test:hook": "NODE_ENV=test jest ./tests/functional/hook/index.test.ts --detectOpenHandles --forceExit --verbose",
47
48
  "test:interrupt": "NODE_ENV=test jest ./tests/functional/interrupt/index.test.ts --detectOpenHandles --forceExit --verbose",
48
49
  "test:parallel": "NODE_ENV=test jest ./tests/functional/parallel/index.test.ts --detectOpenHandles --forceExit --verbose",
49
50
  "test:pipe": "NODE_ENV=test jest ./tests/unit/services/pipe/index.test.ts --detectOpenHandles --forceExit --verbose",
50
- "test:quorum": "NODE_ENV=test jest ./tests/functional/quorum/index.test.ts --detectOpenHandles --forceExit --verbose",
51
+ "test:quorum": "HMSH_IS_CLUSTER=true NODE_ENV=test jest ./tests/functional/quorum/index.test.ts --detectOpenHandles --forceExit --verbose",
51
52
  "test:reclaim": "NODE_ENV=test jest ./tests/functional/reclaim/index.test.ts --detectOpenHandles --forceExit --verbose",
52
53
  "test:redeploy": "NODE_ENV=test jest ./tests/functional/redeploy/index.test.ts --detectOpenHandles --forceExit --verbose",
53
54
  "test:reentrant": "NODE_ENV=test jest ./tests/functional/reentrant/index.test.ts --detectOpenHandles --forceExit --verbose",
package/types/durable.ts CHANGED
@@ -200,7 +200,8 @@ export type WorkflowSearchSchema = Record<
200
200
  * literal value to use for the indexed field name (without including the standard underscore (_) prefix isolate)
201
201
  */
202
202
  fieldName?: string;
203
- }>;
203
+ }
204
+ >;
204
205
 
205
206
  type WorkflowSearchOptions = {
206
207
  /** FT index name (myapp:myindex) */
@@ -312,6 +313,18 @@ type WorkflowOptions = {
312
313
  * default is true; if false, will not await the execution
313
314
  */
314
315
  await?: boolean;
316
+
317
+ /**
318
+ * If provided, the job will initialize in an expired state, reserving
319
+ * only the job ID (HSETNX) and persisting search and marker (if provided).
320
+ * If a `resume` signal is sent before the specified number of seconds,
321
+ * the job will resume as normal, transition to the adjacent children
322
+ * of the trigger. If the job is not resumed within the number
323
+ * of seconds specified, the job will be scrubbed. No dependencies
324
+ * are added for a job in an expired state; however, dependencies
325
+ * will be added after the job is resumed if relevant.
326
+ */
327
+ expired?: number;
315
328
  };
316
329
 
317
330
  /**
package/types/hotmesh.ts CHANGED
@@ -11,7 +11,7 @@ import { LogLevel } from './logger';
11
11
  */
12
12
  enum KeyType {
13
13
  APP = 'APP',
14
- ENGINE_ID = 'ENGINE',
14
+ THROTTLE_RATE = 'THROTTLE_RATE',
15
15
  HOOKS = 'HOOKS',
16
16
  JOB_DEPENDENTS = 'JOB_DEPENDENTS',
17
17
  JOB_STATE = 'JOB_STATE',
package/types/job.ts CHANGED
@@ -106,6 +106,18 @@ type ExtensionType = {
106
106
  * the initial data set.
107
107
  */
108
108
  marker?: StringStringType;
109
+
110
+ /**
111
+ * If provided, the job will initialize in an expired state, reserving
112
+ * only the job ID (HSETNX) and persisting search and marker (if provided).
113
+ * If a `resume` signal is sent before the specified number of seconds,
114
+ * the job will resume as normal, transition the the adjacent children
115
+ * of the trigger. If the job is not resumed within the
116
+ * number of seconds specified, the job will be scrubbed. No dependencies
117
+ * are added for a job in an expired state; however, dependencies will be
118
+ * added after the job is resumed if relevant.
119
+ */
120
+ expired?: number;
109
121
  };
110
122
 
111
123
  /**
package/types/quorum.ts CHANGED
@@ -34,11 +34,9 @@ export type ThrottleOptions = {
34
34
  topic?: string;
35
35
  /** entity/noun */
36
36
  entity?: string;
37
- /** in milliseconds; default is 0 */
37
+ /** in milliseconds; set to -1 for indefinite throttle */
38
38
  throttle: number;
39
- /** namespace
40
- * @default 'durable'
41
- */
39
+ /** namespace */
42
40
  namespace?: string;
43
41
  };
44
42
 
package/types/redis.ts CHANGED
@@ -74,6 +74,7 @@ interface RedisRedisMultiType {
74
74
  XLEN(key: string): this;
75
75
  DEL(key: string): this;
76
76
  EXISTS(key: string): this;
77
+ EXPIRE(key: string, seconds: number): this;
77
78
  HDEL(key: string, itemId: string): this;
78
79
  HGET(key: string, itemId: string): this;
79
80
  HGETALL(key: string): this;
@@ -303,6 +304,8 @@ interface IORedisMultiType {
303
304
  id: string,
304
305
  ...args: string[]
305
306
  ): this;
307
+ del(key: string): this;
308
+ expire(key: string, seconds: number): this;
306
309
  hdel(key: string, itemId: string): this;
307
310
  hget(key: string, itemId: string): this;
308
311
  hgetall(key: string): this;
package/types/stream.ts CHANGED
@@ -130,6 +130,8 @@ export type StreamConfig = {
130
130
  guid: string;
131
131
  /** Role associated with the stream */
132
132
  role: StreamRole;
133
+ /** Default throttle (read from KeyType.THROTTLE_RATE) */
134
+ throttle: number;
133
135
  /** Optional topic for the stream */
134
136
  topic?: string;
135
137
  /** Delay before a message can be reclaimed, defaults to 60,000 milliseconds */