@hotmeshio/hotmesh 0.2.1 → 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
@@ -9,8 +9,8 @@ npm install @hotmeshio/hotmesh
9
9
  ```
10
10
  You have a Redis instance? Good. You're ready to go.
11
11
 
12
- ## SDK Docs
13
- [Read the Docs](https://hotmeshio.github.io/sdk-typescript/)
12
+ ## Learn
13
+ [SDK Docs](https://hotmeshio.github.io/sdk-typescript/) | [Samples](https://github.com/hotmeshio/samples-typescript) | [Intro Video](https://www.loom.com/share/211bd4b4038d42f0ba34374ef5b6f961?sid=7b889a56-f60f-4ccc-84e7-8c2697e548a9)
14
14
 
15
15
  ## MeshCall | Connect Everything
16
16
  [MeshCall](https://hotmeshio.github.io/sdk-typescript/classes/services_meshcall.MeshCall.html) connects your functions to the Redis-backed mesh, exposing them as idempotent endpoints. Function responses are cacheable and functions can even run as idempotent cron jobs. Make blazing fast interservice calls that return in milliseconds without the overhead of HTTP.
@@ -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>
@@ -694,9 +695,7 @@ HotMesh's telemetry output provides unmatched insight into long-running, multi-s
694
695
  <img src="./docs/img/visualize/opentelemetry.png" alt="Open Telemetry" style="width:600px;max-width:600px;">
695
696
 
696
697
  ## Visualize | HotMesh Dashboard
697
- The HotMesh dashboard provides a detailed overview of all running workflows. As HotMesh is a service mesh, it's also possible to throttle and pause workers and engines attached to the mesh. Redis will simply inflate like a balloon until the throttle is removed and ingestion is resumed.
698
-
699
- An LLM is included to simplify querying and analyzing workflow data for those deployments that include the Redis `FT.SEARCH` module.
698
+ The HotMesh dashboard provides a detailed overview of all running workflows. An LLM is included to simplify querying and analyzing workflow data for those deployments that include the Redis `FT.SEARCH` module.
700
699
 
701
700
  <img src="./docs/img/visualize/hotmesh_dashboard.png" alt="HotMesh Dashboard" style="width:600px;max-width:600px;">
702
701
 
@@ -705,35 +704,8 @@ View commands, streams, data, CPU, load, etc using the RedisInsight data browser
705
704
 
706
705
  <img src="./docs/img/visualize/redisinsight.png" alt="Redis Insight" style="width:600px;max-width:600px;">
707
706
 
708
- ## HotMesh
709
- The *MeshData*, *MeshCall*, and *MeshFlow* modules are all created using the HotMesh modeling system. Refer to the following documents to better understand the platform and how it delivers workflow orchestration without a central application server.
710
-
711
- ### FAQ
712
- Refer to the [FAQ](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/faq.md) for terminology, definitions, and an exploration of how HotMesh facilitates orchestration use cases.
713
-
714
- ### Quick Start
715
- Refer to the [Quick Start](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/quickstart.md) for sample YAML workflows you can copy, paste, and modify to get started with HotMesh.
716
-
717
- ### Developer Guide
718
- For more details on the complete development process, including information about schemas, APIs, and deployment, consult the [Developer Guide](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/developer_guide.md).
719
-
720
- ### Model Driven Development
721
- [Model Driven Development](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/model_driven_development.md) is an established strategy for managing process-oriented tasks. Check out this guide to understand its foundational principles.
722
-
723
- ### Data Mapping
724
- Exchanging data between activities is central to HotMesh. For detailed information on supported functions and the functional mapping syntax (@pipes), see the [Data Mapping Overview](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/data_mapping.md).
725
-
726
- ### Composition
727
- While the simplest graphs are linear, detailing a consistent sequence of non-cyclical activities, graphs can be layered to represent intricate business scenarios. Some can even be designed to accommodate long-lasting workflows that span months. For more details, check out the [Composable Workflow Guide](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/composable_workflow.md).
728
-
729
- ### System Lifecycle
730
- Gain insight into HotMesh's monitoring, exception handling, and alarm configurations via the [System Lifecycle Guide](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/system_lifecycle.md).
731
-
732
- ### Distributed Orchestration | System Overview
733
- HotMesh is a distributed orchestration engine. Refer to the [Distributed Orchestration Guide](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/distributed_orchestration.md) for a high-level overview of the approach.
734
-
735
- ### Distributed Orchestration | System Design
736
- HotMesh is more than Redis and TypeScript. The theory that underlies the architecture is applicable to any number of data storage and streaming backends: [A Message-Oriented Approach to Decentralized Process Orchestration](https://zenodo.org/records/12168558).
707
+ ## Samples
708
+ Refer to the [hotmeshio/samples-typescript](https://github.com/hotmeshio/samples-typescript) Git repo for *tutorials* and instructions on deploying the *HotMesh Dashboard*.
737
709
 
738
- ### Samples
739
- Refer to the [hotmeshio/samples-javascript](https://github.com/hotmeshio/samples-javascript) repo for usage examples.
710
+ ## Advanced
711
+ For more advanced topics, including details on the underlying modeling and design system (HotMesh) refer to the [Advanced README](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/advanced.md).
@@ -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.1",
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.1",
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
  };