@hotmeshio/hotmesh 0.2.2 → 0.2.3

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.
package/README.md CHANGED
@@ -19,9 +19,9 @@ You have a Redis instance? Good. You're ready to go.
19
19
  <summary style="font-size:1.25em;">Run an idempotent cron job</summary>
20
20
 
21
21
  ### Run a Cron
22
- This example demonstrates an *idempotent* cron that runs every day. The `id` makes each cron job unique and ensures that only one instance runs, despite repeated invocations. *The `cron` method returns `false` if a workflow is already running with the same `id`.*
22
+ This example demonstrates an *idempotent* cron that runs daily at midnight. The `id` makes each cron job unique and ensures that only one instance runs, despite repeated invocations. *The `cron` method returns `false` if a workflow is already running with the same `id`.*
23
23
 
24
- Optionally set a `delay` and/or set `maxCycles` to limit the number of cycles.
24
+ Optionally set a `delay` and/or set `maxCycles` to limit the number of cycles. The `interval` can be any human-readable time format (e.g., `1 day`, `2 hours`, `30 minutes`, etc) or a standard cron expression.
25
25
 
26
26
  1. Define the cron function.
27
27
  ```typescript
@@ -29,7 +29,7 @@ You have a Redis instance? Good. You're ready to go.
29
29
  import { MeshCall } from '@hotmeshio/hotmesh';
30
30
  import * as Redis from 'redis';
31
31
 
32
- export const runMyCron = async (id: string, interval = '1 day'): Promise<boolean> => {
32
+ export const runMyCron = async (id: string, interval = '0 0 * * *'): Promise<boolean> => {
33
33
  return await MeshCall.cron({
34
34
  topic: 'my.cron.function',
35
35
  redis: {
@@ -48,7 +48,8 @@ You have a Redis instance? Good. You're ready to go.
48
48
  ```typescript
49
49
  //server.ts
50
50
  import { runMyCron } from './cron';
51
- runMyCron('myDailyCron123');
51
+
52
+ runMyCron('myNightlyCron123');
52
53
  ```
53
54
  </details>
54
55
 
@@ -69,7 +70,7 @@ You have a Redis instance? Good. You're ready to go.
69
70
  class: Redis,
70
71
  options: { url: 'redis://:key_admin@redis:6379' }
71
72
  },
72
- options: { id: 'myDailyCron123' }
73
+ options: { id: 'myNightlyCron123' }
73
74
  });
74
75
  ```
75
76
  </details>
@@ -35,3 +35,4 @@ export declare function getValueByPath(obj: {
35
35
  [key: string]: any;
36
36
  }, path: string): any;
37
37
  export declare function restoreHierarchy(obj: StringAnyType): StringAnyType;
38
+ export declare function isValidCron(cronExpression: string): boolean;
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.restoreHierarchy = exports.getValueByPath = exports.getIndexedHash = exports.getSymVal = exports.getSymKey = exports.formatISODate = exports.getTimeSeries = exports.getSubscriptionTopic = exports.findSubscriptionForTrigger = exports.findTopKey = exports.matchesStatus = exports.matchesStatusCode = exports.identifyRedisTypeFromClass = exports.polyfill = exports.identifyRedisType = exports.XSleepFor = exports.sleepImmediate = exports.sleepFor = exports.guid = exports.deterministicRandom = exports.deepCopy = exports.getSystemHealth = exports.hashOptions = void 0;
6
+ exports.isValidCron = exports.restoreHierarchy = exports.getValueByPath = exports.getIndexedHash = exports.getSymVal = exports.getSymKey = exports.formatISODate = exports.getTimeSeries = exports.getSubscriptionTopic = exports.findSubscriptionForTrigger = exports.findTopKey = exports.matchesStatus = exports.matchesStatusCode = exports.identifyRedisTypeFromClass = exports.polyfill = exports.identifyRedisType = exports.XSleepFor = exports.sleepImmediate = exports.sleepFor = exports.guid = exports.deterministicRandom = exports.deepCopy = exports.getSystemHealth = exports.hashOptions = void 0;
7
7
  const os_1 = __importDefault(require("os"));
8
8
  const crypto_1 = require("crypto");
9
9
  const nanoid_1 = require("nanoid");
@@ -239,3 +239,8 @@ function restoreHierarchy(obj) {
239
239
  return result;
240
240
  }
241
241
  exports.restoreHierarchy = restoreHierarchy;
242
+ function isValidCron(cronExpression) {
243
+ const cronRegex = /^(\*|([0-5]?\d)) (\*|([01]?\d|2[0-3])) (\*|([12]?\d|3[01])) (\*|([1-9]|1[0-2])) (\*|([0-6](?:-[0-6])?(?:,[0-6])?))$/;
244
+ return cronRegex.test(cronExpression);
245
+ }
246
+ exports.isValidCron = isValidCron;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -83,6 +83,7 @@
83
83
  "dependencies": {
84
84
  "@apidevtools/json-schema-ref-parser": "^10.1.0",
85
85
  "@opentelemetry/api": "^1.4.1",
86
+ "cron-parser": "^4.9.0",
86
87
  "js-yaml": "^4.1.0",
87
88
  "ms": "^2.1.3",
88
89
  "nanoid": "^3.3.6",
@@ -10,6 +10,7 @@ const hotmesh_1 = require("../hotmesh");
10
10
  const enums_1 = require("../../modules/enums");
11
11
  const utils_1 = require("../../modules/utils");
12
12
  const key_1 = require("../../modules/key");
13
+ const cron_1 = require("../pipe/functions/cron");
13
14
  const factory_1 = require("./schemas/factory");
14
15
  class MeshCall {
15
16
  constructor() { }
@@ -34,7 +35,7 @@ class MeshCall {
34
35
  }
35
36
  return true;
36
37
  }
37
- static async activateWorkflow(hotMesh, appId = key_1.HMNS, version = '1') {
38
+ static async activateWorkflow(hotMesh, appId = key_1.HMNS, version = factory_1.VERSION) {
38
39
  const app = await hotMesh.engine.store.getApp(appId);
39
40
  const appVersion = app?.version;
40
41
  if (appVersion === version && !app.active) {
@@ -140,10 +141,19 @@ class MeshCall {
140
141
  namespace: params.namespace,
141
142
  });
142
143
  const TOPIC = `${params.namespace ?? key_1.HMNS}.cron`;
143
- const interval = (0, ms_1.default)(params.options.interval) / 1000;
144
- const delay = params.options.delay
144
+ let delay = params.options.delay
145
145
  ? (0, ms_1.default)(params.options.delay) / 1000
146
146
  : undefined;
147
+ let cron;
148
+ let interval = enums_1.HMSH_FIDELITY_SECONDS;
149
+ if ((0, utils_1.isValidCron)(params.options.interval)) {
150
+ cron = params.options.interval;
151
+ delay = Math.max(new cron_1.CronHandler().nextDelay(cron), 0);
152
+ }
153
+ else {
154
+ const seconds = (0, ms_1.default)(params.options.interval) / 1000;
155
+ interval = Math.max(seconds, enums_1.HMSH_FIDELITY_SECONDS);
156
+ }
147
157
  const maxCycles = params.options.maxCycles ?? 1000000;
148
158
  const hotMeshInstance = await MeshCall.getInstance(params.namespace, params.redis);
149
159
  await hotMeshInstance.pub(TOPIC, {
@@ -151,6 +161,7 @@ class MeshCall {
151
161
  topic: params.topic,
152
162
  args: params.args,
153
163
  interval,
164
+ cron,
154
165
  maxCycles,
155
166
  delay,
156
167
  });
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "1";
1
+ export declare const VERSION = "2";
2
2
  export declare const getWorkflowYAML: (appId?: string, version?: string) => string;
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getWorkflowYAML = exports.VERSION = void 0;
4
4
  const key_1 = require("../../../modules/key");
5
- exports.VERSION = '1';
5
+ exports.VERSION = '2';
6
6
  const getWorkflowYAML = (appId = key_1.HMNS, version = exports.VERSION) => {
7
7
  return `app:
8
8
  id: ${appId}
@@ -86,7 +86,10 @@ const getWorkflowYAML = (appId = key_1.HMNS, version = exports.VERSION) => {
86
86
  description: time in seconds to sleep before invoking the first cycle
87
87
  interval:
88
88
  type: number
89
- description: time in seconds to sleep before the next cycle
89
+ description: time in seconds to sleep before the next cycle (also min interval in seconds if cron is provided)
90
+ cron:
91
+ type: string
92
+ description: cron expression to determine the next cycle (takes precedence over interval)
90
93
  topic:
91
94
  type: string
92
95
  description: topic assigned to locate the worker
@@ -148,7 +151,11 @@ const getWorkflowYAML = (appId = key_1.HMNS, version = exports.VERSION) => {
148
151
  ancestor: cycle_hook_cron
149
152
  input:
150
153
  maps:
151
- sleepSeconds: '{trigger_cron.output.data.interval}'
154
+ sleepSeconds:
155
+ '@pipe':
156
+ - ['{trigger_cron.output.data.cron}']
157
+ - ['{@cron.nextDelay}', '{trigger_cron.output.data.interval}']
158
+ - ['{@math.max}']
152
159
  iterationCount:
153
160
  '@pipe':
154
161
  - ['{cycle_hook_cron.output.data.iterationCount}', 1]
@@ -0,0 +1,4 @@
1
+ declare class CronHandler {
2
+ nextDelay(cronExpression: string): number;
3
+ }
4
+ export { CronHandler };
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CronHandler = void 0;
4
+ const cron_parser_1 = require("cron-parser");
5
+ const enums_1 = require("../../../modules/enums");
6
+ const utils_1 = require("../../../modules/utils");
7
+ class CronHandler {
8
+ nextDelay(cronExpression) {
9
+ try {
10
+ if (!(0, utils_1.isValidCron)(cronExpression)) {
11
+ return -1;
12
+ }
13
+ const interval = (0, cron_parser_1.parseExpression)(cronExpression, { utc: true });
14
+ const nextDate = interval.next().toDate();
15
+ const now = new Date();
16
+ const delay = (nextDate.getTime() - now.getTime()) / 1000;
17
+ if (delay <= 0) {
18
+ return -1;
19
+ }
20
+ if (delay < enums_1.HMSH_FIDELITY_SECONDS) {
21
+ return enums_1.HMSH_FIDELITY_SECONDS;
22
+ }
23
+ const iDelay = Math.round(delay);
24
+ return iDelay;
25
+ }
26
+ catch (error) {
27
+ console.error('Error calculating next cron job execution delay:', error);
28
+ return -1;
29
+ }
30
+ }
31
+ }
32
+ exports.CronHandler = CronHandler;
@@ -1,6 +1,7 @@
1
1
  import { ArrayHandler } from './array';
2
2
  import { BitwiseHandler } from './bitwise';
3
3
  import { ConditionalHandler } from './conditional';
4
+ import { CronHandler } from './cron';
4
5
  import { DateHandler } from './date';
5
6
  import { JsonHandler } from './json';
6
7
  import { LogicalHandler } from './logical';
@@ -14,6 +15,7 @@ declare const _default: {
14
15
  array: ArrayHandler;
15
16
  bitwise: BitwiseHandler;
16
17
  conditional: ConditionalHandler;
18
+ cron: CronHandler;
17
19
  date: DateHandler;
18
20
  json: JsonHandler;
19
21
  logical: LogicalHandler;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const array_1 = require("./array");
4
4
  const bitwise_1 = require("./bitwise");
5
5
  const conditional_1 = require("./conditional");
6
+ const cron_1 = require("./cron");
6
7
  const date_1 = require("./date");
7
8
  const json_1 = require("./json");
8
9
  const logical_1 = require("./logical");
@@ -16,6 +17,7 @@ exports.default = {
16
17
  array: new array_1.ArrayHandler(),
17
18
  bitwise: new bitwise_1.BitwiseHandler(),
18
19
  conditional: new conditional_1.ConditionalHandler(),
20
+ cron: new cron_1.CronHandler(),
19
21
  date: new date_1.DateHandler(),
20
22
  json: new json_1.JsonHandler(),
21
23
  logical: new logical_1.LogicalHandler(),
@@ -26,6 +26,7 @@ declare class Router {
26
26
  innerPromiseResolve: (() => void) | null;
27
27
  isSleeping: boolean;
28
28
  sleepTimout: NodeJS.Timeout | null;
29
+ readonly: boolean;
29
30
  constructor(config: StreamConfig, stream: StreamService<RedisClient, RedisMulti>, store: StoreService<RedisClient, RedisMulti>, logger: ILogger);
30
31
  private resetThrottleState;
31
32
  createGroup(stream: string, group: string): Promise<void>;
@@ -26,6 +26,7 @@ class Router {
26
26
  this.reclaimDelay = config.reclaimDelay || enums_1.HMSH_XCLAIM_DELAY_MS;
27
27
  this.reclaimCount = config.reclaimCount || enums_1.HMSH_XCLAIM_COUNT;
28
28
  this.logger = logger;
29
+ this.readonly = config.readonly || false;
29
30
  this.resetThrottleState();
30
31
  }
31
32
  resetThrottleState() {
@@ -73,6 +74,8 @@ class Router {
73
74
  });
74
75
  }
75
76
  async consumeMessages(stream, group, consumer, callback) {
77
+ if (this.readonly)
78
+ return;
76
79
  this.logger.info(`router-stream-starting`, { group, consumer, stream });
77
80
  Router.instances.add(this);
78
81
  this.shouldConsume = true;
@@ -50,6 +50,7 @@ type HotMeshEngine = {
50
50
  redis?: RedisConfig;
51
51
  reclaimDelay?: number;
52
52
  reclaimCount?: number;
53
+ readonly?: boolean;
53
54
  };
54
55
  type HotMeshWorker = {
55
56
  topic: string;
@@ -71,4 +71,5 @@ export type StreamConfig = {
71
71
  topic?: string;
72
72
  reclaimDelay?: number;
73
73
  reclaimCount?: number;
74
+ readonly?: boolean;
74
75
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -83,6 +83,7 @@
83
83
  "dependencies": {
84
84
  "@apidevtools/json-schema-ref-parser": "^10.1.0",
85
85
  "@opentelemetry/api": "^1.4.1",
86
+ "cron-parser": "^4.9.0",
86
87
  "js-yaml": "^4.1.0",
87
88
  "ms": "^2.1.3",
88
89
  "nanoid": "^3.3.6",
package/types/hotmesh.ts CHANGED
@@ -62,6 +62,7 @@ type HotMeshEngine = {
62
62
  redis?: RedisConfig;
63
63
  reclaimDelay?: number; //milliseconds
64
64
  reclaimCount?: number;
65
+ readonly?: boolean; //if true, the engine will not route stream messages
65
66
  };
66
67
 
67
68
  type HotMeshWorker = {
package/types/meshcall.ts CHANGED
@@ -102,6 +102,7 @@ interface MeshCallCronOptions {
102
102
  /**
103
103
  * For example, `1 day`, `1 hour`. Fidelity is generally
104
104
  * within 5 seconds. Refer to the syntax for the `ms` NPM package.
105
+ * Standard cron syntax is also supported. (e.g. `0 0 * * *`)
105
106
  */
106
107
  interval: string;
107
108
  /**
@@ -112,6 +113,7 @@ interface MeshCallCronOptions {
112
113
  * Time in seconds to sleep before invoking the first cycle.
113
114
  * For example, `1 day`, `1 hour`. Fidelity is generally
114
115
  * within 5 seconds. Refer to the syntax for the `ms` NPM package.
116
+ * If the interval field uses standard cron syntax, this field is ignored.
115
117
  */
116
118
  delay?: string;
117
119
  }
package/types/stream.ts CHANGED
@@ -138,4 +138,6 @@ export type StreamConfig = {
138
138
  reclaimDelay?: number;
139
139
  /** Maximum number of reclaims allowed, defaults to 3. Values greater throw an error */
140
140
  reclaimCount?: number;
141
+ /** if true, will not process stream messages; default true */
142
+ readonly?: boolean;
141
143
  };