@hotmeshio/hotmesh 0.0.24 → 0.0.26

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
@@ -1,14 +1,11 @@
1
1
  # HotMesh
2
2
  ![alpha release](https://img.shields.io/badge/release-alpha-yellow)
3
3
 
4
- Elevate Redis from an in-memory data cache to a game-changing [service mesh](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/faq.md#what-is-hotmesh). Turn your unpredictable functions into unbreakable workflows.
4
+ Elevate Redis from an in-memory data cache, and turn your unpredictable functions into unbreakable workflows.
5
5
 
6
- ## What is HotMesh?
7
- HotMesh is a wrapper for Redis that exposes a higher level set of domain constructs like ‘activities’, ‘workflows’, 'jobs', etc. Behind the scenes, it uses *Redis Data* (Hash, ZSet, and List); *Redis Streams* (XReadGroup, XAdd, XLen); and *Redis Publish/Subscribe*.
6
+ **HotMesh** is a wrapper for Redis that exposes a higher level set of domain constructs like ‘activities’, ‘workflows’, 'jobs', etc. Behind the scenes, it uses *Redis Data* (Hash, ZSet, List); *Redis Streams* (XReadGroup, XAdd, XLen, etc); and *Redis Publish/Subscribe*.
8
7
 
9
- The ultimate goal is to resurface Redis as a *Durable Service Mesh*, capable of running unbreakable workflows that span your microservices. The technical term for this type of durability is *Reentrant Process Engine*.
10
-
11
- It's still Redis in the background, but the information flow is fundamentally different. Your functions no longer call Redis (e.g., to cache a document) and instead are called by Redis.
8
+ It's still Redis in the background, but the information flow is reversed. Instead of your functions calling Redis (e.g., for caching a document), Redis governs your function execution. If your microservice container goes down or your function simply fails, HotMesh will restore function state at the point of failure and retry until it succeeds.
12
9
 
13
10
  ## Install
14
11
  [![npm version](https://badge.fury.io/js/%40hotmeshio%2Fhotmesh.svg)](https://badge.fury.io/js/%40hotmeshio%2Fhotmesh)
@@ -20,34 +17,32 @@ npm install @hotmeshio/hotmesh
20
17
  ## Design
21
18
  The HotMesh SDK is designed to keep your code front-and-center. Write code as you normally would, then use HotMesh to make it durable.
22
19
 
23
- 1. Start with any ordinary class. Pay attention to unpredictable functions: those that execute slowly, cause problems at scale, or simply fail to return. *Note how the `saludar` function throws an error 50% of the time. This is exactly the type of function to fix.*
20
+ 1. Start with any ordinary class. Pay attention to unpredictable functions: those that execute slowly, cause problems at scale, or simply fail to return. *Note how the `flaky` function throws an error 50% of the time. This is exactly the type of function that can be fixed using HotMesh.*
24
21
  ```javascript
25
22
  //myworkflow.ts
26
23
 
27
24
  export class MyWorkflow {
28
25
 
29
- //main method
30
26
  async run(name: string): Promise<string> {
31
- const salud = await this.saludar(name);
27
+ const hi = await this.flaky(name);
32
28
  const hello = await this.greet(name);
33
- return `${hello} ${salud}`;
29
+ return `${hi} ${hello}`;
34
30
  }
35
31
 
36
- //this function only succeeds 50% of the time!
37
- async saludar(nombre: string): Promise<string> {
32
+ //this function is unpredictable and will fail 50% of the time
33
+ async flaky(name: string): Promise<string> {
38
34
  if (Math.random() < 0.5) {
39
- throw new Error('¡No hablo español!');
35
+ throw new Error('Ooops!');
40
36
  }
41
- return `¡Hola, ${nombre}!`;
37
+ return `Hi, ${name}!`;
42
38
  }
43
39
 
44
- //this function always succeeds
45
40
  async greet(name: string): Promise<string> {
46
41
  return `Hello, ${name}!`;
47
42
  }
48
43
  }
49
44
  ```
50
- 2. Import `MeshOS` and subclass it as shown. Configure `host`, `port`, etc., and list those functions that should run durably like `run` and `saludar`. Your class is now a durable workflow! *Additional helper methods include `waitForSignal`, `signal`, `hook`, `sleep` (months, years, etc), `executeChild`, `startChild`, `get`, `set`, `incr` (increment), and `mult` (multiply).*
45
+ 2. Import `Redis` and `MeshOS` and configure host, port, etc. List those functions that Redis should govern as durable workflows (like `run` and `flaky`). And that's it! *Your functions don't actually change; rather, their governance does.*
51
46
  ```javascript
52
47
  //myworkflow.ts
53
48
 
@@ -63,40 +58,52 @@ The HotMesh SDK is designed to keep your code front-and-center. Write code as yo
63
58
  workflowFunctions = ['run'];
64
59
 
65
60
  //list functions to retry and cache
66
- proxyFunctions = ['saludar'];
61
+ proxyFunctions = ['flaky'];
67
62
 
68
- //main method (Redis will now govern its execution)
69
63
  async run(name: string): Promise<string> {
70
- const salud = await this.saludar(name);
64
+ const hi = await this.flaky(name);
71
65
  const hello = await this.greet(name);
72
- return `${hello} ${salud}`;
66
+ return `${hi} ${hello}`;
73
67
  }
74
68
 
75
- //this function will now be retried until it succeeds;
76
- async saludar(nombre: string): Promise<string> {
69
+ //this function is now durable and will be retried until it succeeds!
70
+ async flaky(name: string): Promise<string> {
77
71
  if (Math.random() < 0.5) {
78
- throw new Error('¡No hablo español!');
72
+ throw new Error('Ooops!');
79
73
  }
80
- return `¡Hola, ${nombre}!`;
74
+ return `Hi, ${name}!`;
81
75
  }
82
76
 
83
- //this vanilla function is unchanged
84
77
  async greet(name: string): Promise<string> {
85
78
  return `Hello, ${name}!`;
86
79
  }
87
80
  }
88
81
  ```
89
- 3. Invoke your class, providing a unique GUID (it's now a workflow and it's idempotent). Nothing changes from the outside, *but Redis now governs its end-to-end execution.* It's guaranteed to succeed and return regardless of catastrophic network failure. Redis will simply inflate like a balloon and deflate as things come back online.
82
+ 3. Invoke your class, providing a unique id (it's now an idempotent workflow and needs a GUID). Nothing changes from the outside, *but Redis now governs the end-to-end execution.* It's guaranteed to succeed, even if it takes a while.
90
83
  ```javascript
91
84
  //mycaller.ts
92
85
 
93
- //...import MyWorkflow, etc...
94
-
95
- const workflow = new MyWorkflow('unique123', { await: true }); //await the response
96
- const response = await workflow.run('John');
97
- //Hello, John! ¡Hola, John!
86
+ const workflow = new MyWorkflow({ id: 'my123', await: true });
87
+ const response = await workflow.run('World');
88
+ //Hi, World! Hello, World!
98
89
  ```
99
90
 
91
+ Redis governance delivers more than just reliability. Externalizing state fundamentally changes the execution profile for your functions, allowing you to design long-running, durable workflows. Use the following methods to solve the most common state management challenges.
92
+
93
+ - `waitForSignal` | Pause and wait for external event(s) before continuing. The *waitForSignal* method will collate and cache the signals and only awaken your function once they've all arrived.
94
+ - `signal` | Send a signal (and optional payload) to any paused function.
95
+ - `hook` | Redis governance supercharges your functions, transforming them into 're-entrant processes'. Optionally use the *hook* method to spawn parallel execution threads within any running function.
96
+ - `sleep` | Pause function execution for a ridiculous amount of time (months, years, etc). There's no risk of information loss, as Redis governs function state. When your function awakens, function state is efficiently (and automatically) restored.
97
+ - `random` | Generate a deterministic random number that can be used in a reentrant process workflow (replaces `Math.random()`).
98
+ - `executeChild` | Call another durable function and await the response. *Design sophisticated, multi-process solutions by leveraging this command.*
99
+ - `startChild` | Call another durable function, but do not await the response.
100
+ - `set` | Set a value (e.g, `set('name', 'value')`)
101
+ - `get` | Get a value (e.g, `get('name')`)
102
+ - `incr` | Increment (or decrement) a number (e.g, `incr('name', -99)`)
103
+ - `mult` | Multiply (or divide) a number (e.g, `mult('name', 12)`)
104
+
105
+ Refer to the [hotmeshio/samples-typescript](https://github.com/hotmeshio/samples-typescript) repo for usage examples.
106
+
100
107
  ## Advanced Design
101
108
  HotMesh's TypeScript SDK is the easiest way to make your functions durable. But if you need full control over your function lifecycles (including high-volume, high-speed use cases), you can use HotMesh's underlying YAML models to optimize your durable workflows. The following model depicts a sequence of activities orchestrated by HotMesh. Any function you associate with a `topic` in your YAML definition is guaranteed to be durable.
102
109
 
@@ -232,4 +239,4 @@ HotMesh is a distributed orchestration engine. Refer to the [Distributed Orchest
232
239
  Gain insight into HotMesh's monitoring, exception handling, and alarm configurations via the [System Lifecycle Guide](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/system_lifecycle.md).
233
240
 
234
241
  ## Alpha Release
235
- So what exacty is an [alpha release](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/alpha.md)?!
242
+ So what exacty is an [alpha release](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/alpha.md)?
@@ -5,6 +5,7 @@ import { RedisClient, RedisMulti } from "../types/redis";
5
5
  import { StringAnyType } from "../types/serializer";
6
6
  import { StreamCode, StreamStatus } from "../types/stream";
7
7
  export declare function sleepFor(ms: number): Promise<unknown>;
8
+ export declare function deterministicRandom(seed: number): number;
8
9
  export declare function identifyRedisType(redisInstance: any): 'redis' | 'ioredis' | null;
9
10
  export declare const polyfill: {
10
11
  resolveActivityType(activityType: string): string;
@@ -1,10 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.restoreHierarchy = exports.getValueByPath = exports.getIndexedHash = exports.getSymVal = exports.getSymKey = exports.formatISODate = exports.getTimeSeries = exports.getSubscriptionTopic = exports.findSubscriptionForTrigger = exports.findTopKey = exports.XSleepFor = exports.matchesStatus = exports.matchesStatusCode = exports.identifyRedisTypeFromClass = exports.polyfill = exports.identifyRedisType = exports.sleepFor = void 0;
3
+ exports.restoreHierarchy = exports.getValueByPath = exports.getIndexedHash = exports.getSymVal = exports.getSymKey = exports.formatISODate = exports.getTimeSeries = exports.getSubscriptionTopic = exports.findSubscriptionForTrigger = exports.findTopKey = exports.XSleepFor = exports.matchesStatus = exports.matchesStatusCode = exports.identifyRedisTypeFromClass = exports.polyfill = exports.identifyRedisType = exports.deterministicRandom = exports.sleepFor = void 0;
4
4
  async function sleepFor(ms) {
5
5
  return new Promise((resolve) => setTimeout(resolve, ms));
6
6
  }
7
7
  exports.sleepFor = sleepFor;
8
+ function deterministicRandom(seed) {
9
+ let x = Math.sin(seed) * 10000;
10
+ return x - Math.floor(x);
11
+ }
12
+ exports.deterministicRandom = deterministicRandom;
8
13
  function identifyRedisType(redisInstance) {
9
14
  const prototype = Object.getPrototypeOf(redisInstance);
10
15
  if ('defineCommand' in prototype || Object.keys(prototype).includes('multi')) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.24",
3
+ "version": "0.0.26",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -1,12 +1,12 @@
1
1
  import { WorkflowHandleService } from './handle';
2
- import { FindOptions, MeshOSActivityOptions, MeshOSOptions, WorkflowSearchOptions } from '../../types/durable';
3
- import { RedisOptions, RedisClass } from '../../types/redis';
4
- import { StringAnyType } from '../../types';
5
2
  import { WorkflowService } from './workflow';
3
+ import { FindOptions, FindWhereOptions, FindWhereQuery, MeshOSActivityOptions, MeshOSConfig, MeshOSOptions, MeshOSWorkerOptions, WorkflowSearchOptions } from '../../types/durable';
4
+ import { RedisOptions, RedisClass } from '../../types/redis';
5
+ import { StringAnyType } from '../../types/serializer';
6
6
  /**
7
7
  * The base class for running MeshOS workflows.
8
- * Extend and register subclass methods by name to
9
- * execute as durable workflows, backed by Redis.
8
+ * Extend this class, add your Redis config, and add functions to
9
+ * execute as durable `hooks`, `workflows`, and `activities`.
10
10
  */
11
11
  export declare class MeshOSService {
12
12
  /**
@@ -78,14 +78,18 @@ export declare class MeshOSService {
78
78
  */
79
79
  static createIndex(): Promise<void>;
80
80
  /**
81
- * Initialize the worker(s) for the entity. This is a static
81
+ * stop the workers
82
+ * @returns {Promise<void>}
83
+ */
84
+ static stopWorkers(): Promise<void>;
85
+ /**
86
+ * Initializes the worker(s). This is a static
82
87
  * method that allows for optional task Queue targeting.
83
- * NOTE: Allow List may be optionally used to only wrap
84
- * specific methods in this class.
85
- * @param {string} taskQueue
86
- * @param {string[]} allowList
88
+ * An `allowList` may be optionally provided to start
89
+ * specific `worker` and `hook` methods.
90
+ * @param {MeshOSWorkerOptions} [options]
87
91
  */
88
- static startWorkers(taskQueue?: string, allowList?: Array<MeshOSOptions | string>): Promise<void>;
92
+ static startWorkers(options?: MeshOSWorkerOptions): Promise<void>;
89
93
  /**
90
94
  * executes the redis FT search query
91
95
  * @example '@_quantity:[89 89]'
@@ -93,6 +97,14 @@ export declare class MeshOSService {
93
97
  * @returns {string}
94
98
  */
95
99
  static find(options: FindOptions, ...args: string[]): Promise<string[] | [number]>;
100
+ /**
101
+ * Provides a JSON abstraction for the Redis FT.search command
102
+ * (e.g, `count`, `query`, `return`, `limit`)
103
+ * @param {FindWhereOptions} options
104
+ * @returns {Promise<string[] | [number]>}
105
+ */
106
+ static findWhere(options: FindWhereOptions): Promise<string[] | [number]>;
107
+ static generateSearchQuery(query: FindWhereQuery[]): string;
96
108
  /**
97
109
  * returns the workflow handle. The handle can then be
98
110
  * used to query for status, state, custom state, etc.
@@ -104,5 +116,5 @@ export declare class MeshOSService {
104
116
  * Optionally include a target taskQueue to exec the
105
117
  * workflow's call on a specific worker queue.
106
118
  */
107
- constructor(id?: string, options?: Record<string, any>);
119
+ constructor(id?: string | MeshOSConfig, options?: MeshOSConfig);
108
120
  }
@@ -2,16 +2,17 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MeshOSService = void 0;
4
4
  const nanoid_1 = require("nanoid");
5
+ const _1 = require(".");
6
+ const asyncLocalStorage_1 = require("./asyncLocalStorage");
5
7
  const client_1 = require("./client");
6
8
  const search_1 = require("./search");
7
9
  const worker_1 = require("./worker");
8
- const _1 = require(".");
9
- const asyncLocalStorage_1 = require("./asyncLocalStorage");
10
10
  const workflow_1 = require("./workflow");
11
+ const stream_1 = require("../signaler/stream");
11
12
  /**
12
13
  * The base class for running MeshOS workflows.
13
- * Extend and register subclass methods by name to
14
- * execute as durable workflows, backed by Redis.
14
+ * Extend this class, add your Redis config, and add functions to
15
+ * execute as durable `hooks`, `workflows`, and `activities`.
15
16
  */
16
17
  class MeshOSService {
17
18
  static async getHotMeshClient(redisClass, redisOptions, namespace, taskQueue) {
@@ -42,14 +43,24 @@ class MeshOSService {
42
43
  search_1.Search.configureSearchIndex(hmClient, my.search);
43
44
  }
44
45
  /**
45
- * Initialize the worker(s) for the entity. This is a static
46
+ * stop the workers
47
+ * @returns {Promise<void>}
48
+ */
49
+ static async stopWorkers() {
50
+ await _1.Durable.Client.shutdown();
51
+ await _1.Durable.Worker.shutdown();
52
+ await stream_1.StreamSignaler.stopConsuming();
53
+ }
54
+ /**
55
+ * Initializes the worker(s). This is a static
46
56
  * method that allows for optional task Queue targeting.
47
- * NOTE: Allow List may be optionally used to only wrap
48
- * specific methods in this class.
49
- * @param {string} taskQueue
50
- * @param {string[]} allowList
57
+ * An `allowList` may be optionally provided to start
58
+ * specific `worker` and `hook` methods.
59
+ * @param {MeshOSWorkerOptions} [options]
51
60
  */
52
- static async startWorkers(taskQueue, allowList = []) {
61
+ static async startWorkers(options) {
62
+ const taskQueue = options && options.taskQueue;
63
+ const allowList = options && options.allowList || [];
53
64
  const my = new this();
54
65
  //helper functions
55
66
  const resolveFunctionNames = (arr) => arr.map(item => typeof item === 'string' ? item : item.name);
@@ -74,8 +85,6 @@ class MeshOSService {
74
85
  const proxiedActivities = _1.Durable.workflow.proxyActivities({
75
86
  activities: proxyActivities
76
87
  });
77
- //WATCH!: unsure if this will pollute the scope; don't think
78
- // so as activity functions are terminal in the chain.
79
88
  Object.assign(my, proxiedActivities);
80
89
  }
81
90
  const functionsToIterate = allowList.length ? resolveFunctionNames(allowList) : resolveFunctionNames([...my.workflowFunctions, ...my.hookFunctions]);
@@ -142,9 +151,8 @@ class MeshOSService {
142
151
  class: my.redisClass,
143
152
  options: my.redisOptions
144
153
  } });
145
- //workflow name is the function name driving the workflow
146
154
  let workflowName;
147
- if (options?.workflowName) {
155
+ if (options.workflowName) {
148
156
  workflowName = options?.workflowName;
149
157
  }
150
158
  else if (my.workflowFunctions?.length) {
@@ -156,7 +164,66 @@ class MeshOSService {
156
164
  workflowName = target.name;
157
165
  }
158
166
  }
159
- return await client.workflow.search(options?.taskQueue ?? my.taskQueue, workflowName, my.namespace, my.search.index, ...args); //[count, [id, fields[], id, fields[], id, fields[], ...]]
167
+ return await client.workflow.search(options.taskQueue ?? my.taskQueue, workflowName, options.namespace ?? my.namespace, options.index ?? my.search.index, ...args); //[count, [id, fields[]], [id, fields[]], [id, fields[]], ...]]
168
+ }
169
+ /**
170
+ * Provides a JSON abstraction for the Redis FT.search command
171
+ * (e.g, `count`, `query`, `return`, `limit`)
172
+ * @param {FindWhereOptions} options
173
+ * @returns {Promise<string[] | [number]>}
174
+ */
175
+ static async findWhere(options) {
176
+ const args = [this.generateSearchQuery(options.query)];
177
+ if (options.count) {
178
+ args.push('LIMIT', '0', '0');
179
+ }
180
+ else {
181
+ //limit which hash fields to return
182
+ if (options.return?.length) {
183
+ args.push('RETURN');
184
+ args.push(options.return.length.toString());
185
+ options.return.forEach(returnField => {
186
+ args.push(`_${returnField}`);
187
+ });
188
+ }
189
+ //paginate
190
+ if (options.limit) {
191
+ args.push('LIMIT', options.limit.start.toString(), options.limit.size.toString());
192
+ }
193
+ }
194
+ return await this.find(options.options ?? {}, ...args);
195
+ }
196
+ static generateSearchQuery(query) {
197
+ const my = new this();
198
+ let queryString = query.map(q => {
199
+ const { field, is, value, type } = q;
200
+ const prefixedFieldName = my.search?.schema && field in my.search.schema ? `@_${field}` : `@${field}`;
201
+ const fieldType = my.search?.schema[field]?.type ?? type ?? 'TEXT';
202
+ switch (fieldType) {
203
+ case 'TAG':
204
+ return `${prefixedFieldName}:{${value}}`;
205
+ case 'TEXT':
206
+ return `${prefixedFieldName}:"${value}"`;
207
+ case 'NUMERIC':
208
+ let range = '';
209
+ if (is.startsWith('=')) {
210
+ range = `[${value} ${value}]`;
211
+ }
212
+ else if (is === '<') {
213
+ range = `[-inf ${value}]`;
214
+ }
215
+ else if (is === '>') {
216
+ range = `[${value} +inf]`;
217
+ }
218
+ else if (is === '[]') {
219
+ range = `[${value[0]} ${value[1]}]`;
220
+ }
221
+ return `${prefixedFieldName}:${range}`;
222
+ default:
223
+ return '';
224
+ }
225
+ }).join(' ');
226
+ return queryString;
160
227
  }
161
228
  /**
162
229
  * returns the workflow handle. The handle can then be
@@ -224,7 +291,15 @@ class MeshOSService {
224
291
  password: '',
225
292
  db: 0,
226
293
  };
227
- this.id = id;
294
+ if (typeof (id) === 'string') {
295
+ this.id = id;
296
+ }
297
+ else if (id?.id) {
298
+ this.id = id.id;
299
+ options = id;
300
+ id = undefined;
301
+ }
302
+ ;
228
303
  if (options?.taskQueue) {
229
304
  this.taskQueue = options.taskQueue;
230
305
  }
@@ -28,6 +28,13 @@ export declare class WorkflowService {
28
28
  * process state and job state)
29
29
  */
30
30
  static isSideEffectAllowed(hotMeshClient: HotMesh, prefix: string): Promise<boolean>;
31
+ /**
32
+ * returns a random number between 0 and 1. This number is deterministic
33
+ * and will never vary for a given seed. This is useful for randomizing
34
+ * pathways in a workflow that can be safely replayed.
35
+ * @returns {number}
36
+ */
37
+ static random(): number;
31
38
  /**
32
39
  * send signal data into any other paused thread (which is paused and
33
40
  * awaiting the signal) from within a hook-thread or the main-thread
@@ -14,6 +14,7 @@ const factory_1 = require("./factory");
14
14
  const search_1 = require("./search");
15
15
  const worker_1 = require("./worker");
16
16
  const stream_1 = require("../../types/stream");
17
+ const utils_1 = require("../../modules/utils");
17
18
  class WorkflowService {
18
19
  /**
19
20
  * Spawn a child workflow. await and return the result.
@@ -151,6 +152,18 @@ class WorkflowService {
151
152
  const guidValue = Number(await hotMeshClient.engine.store.exec('HINCRBYFLOAT', workflowGuid, sessionId, '1'));
152
153
  return guidValue === 1;
153
154
  }
155
+ /**
156
+ * returns a random number between 0 and 1. This number is deterministic
157
+ * and will never vary for a given seed. This is useful for randomizing
158
+ * pathways in a workflow that can be safely replayed.
159
+ * @returns {number}
160
+ */
161
+ static random() {
162
+ const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
163
+ const COUNTER = store.get('counter');
164
+ const seed = COUNTER.counter = COUNTER.counter + 1;
165
+ return (0, utils_1.deterministicRandom)(seed);
166
+ }
154
167
  /**
155
168
  * send signal data into any other paused thread (which is paused and
156
169
  * awaiting the signal) from within a hook-thread or the main-thread
@@ -33,8 +33,7 @@ class RedisStoreService extends index_1.StoreService {
33
33
  };
34
34
  }
35
35
  getMulti() {
36
- const multi = this.redisClient.MULTI();
37
- return multi;
36
+ return this.redisClient.multi();
38
37
  }
39
38
  async exec(...args) {
40
39
  return await this.redisClient.sendCommand(args);
@@ -13,7 +13,7 @@ class RedisStreamService extends index_1.StreamService {
13
13
  this.appId = appId;
14
14
  }
15
15
  getMulti() {
16
- return this.redisClient.MULTI();
16
+ return this.redisClient.multi();
17
17
  }
18
18
  mintKey(type, params) {
19
19
  if (!this.namespace)
@@ -13,7 +13,7 @@ class RedisSubService extends index_1.SubService {
13
13
  this.appId = appId;
14
14
  }
15
15
  getMulti() {
16
- const multi = this.redisClient.MULTI();
16
+ const multi = this.redisClient.multi();
17
17
  return multi;
18
18
  }
19
19
  mintKey(type, params) {
@@ -59,15 +59,9 @@ type MeshOSClassConfig = {
59
59
  redisClass: RedisClass;
60
60
  };
61
61
  type MeshOSConfig = {
62
+ id?: string;
63
+ await?: boolean;
62
64
  taskQueue?: string;
63
- index?: {
64
- index: string;
65
- prefix: string[];
66
- schema: Record<string, {
67
- type: 'TEXT' | 'NUMERIC' | 'TAG';
68
- sortable: boolean;
69
- }>;
70
- };
71
65
  };
72
66
  type ConnectionConfig = {
73
67
  class: RedisClass;
@@ -88,12 +82,28 @@ type WorkerConfig = {
88
82
  options?: WorkerOptions;
89
83
  search?: WorkflowSearchOptions;
90
84
  };
85
+ type FindWhereQuery = {
86
+ field: string;
87
+ is: string;
88
+ value: string | boolean | number | [number, number];
89
+ type?: string;
90
+ };
91
91
  type FindOptions = {
92
92
  workflowName?: string;
93
93
  taskQueue?: string;
94
94
  namespace?: string;
95
95
  index?: string;
96
96
  };
97
+ type FindWhereOptions = {
98
+ options?: FindOptions;
99
+ count?: boolean;
100
+ query: FindWhereQuery[];
101
+ return?: string[];
102
+ limit?: {
103
+ start: number;
104
+ size: number;
105
+ };
106
+ };
97
107
  type MeshOSOptions = {
98
108
  name: string;
99
109
  options: WorkerOptions;
@@ -102,6 +112,13 @@ type MeshOSActivityOptions = {
102
112
  name: string;
103
113
  options: ActivityConfig;
104
114
  };
115
+ type MeshOSWorkerOptions = {
116
+ taskQueue?: string;
117
+ allowList?: Array<MeshOSOptions | string>;
118
+ logLevel?: string;
119
+ maxSystemRetries?: number;
120
+ backoffCoefficient?: number;
121
+ };
105
122
  type WorkerOptions = {
106
123
  logLevel?: string;
107
124
  maxSystemRetries?: number;
@@ -125,4 +142,4 @@ type ActivityConfig = {
125
142
  maximumInterval: string;
126
143
  };
127
144
  };
128
- export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection, ProxyType, Registry, SignalOptions, FindOptions, HookOptions, MeshOSActivityOptions, MeshOSClassConfig, MeshOSConfig, MeshOSOptions, WorkerConfig, WorkflowConfig, WorkerOptions, WorkflowSearchOptions, WorkflowDataType, WorkflowOptions, };
145
+ export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection, ProxyType, Registry, SignalOptions, FindOptions, FindWhereOptions, FindWhereQuery, HookOptions, MeshOSActivityOptions, MeshOSWorkerOptions, MeshOSClassConfig, MeshOSConfig, MeshOSOptions, WorkerConfig, WorkflowConfig, WorkerOptions, WorkflowSearchOptions, WorkflowDataType, WorkflowOptions, };
@@ -3,7 +3,7 @@ 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, ProxyType, Registry, SignalOptions, FindOptions, HookOptions, MeshOSActivityOptions, MeshOSClassConfig, MeshOSConfig, MeshOSOptions, WorkflowConfig, WorkerConfig, WorkerOptions, WorkflowSearchOptions, WorkflowDataType, WorkflowOptions, } from './durable';
6
+ export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection, ProxyType, Registry, SignalOptions, FindOptions, FindWhereOptions, FindWhereQuery, HookOptions, MeshOSActivityOptions, MeshOSWorkerOptions, MeshOSClassConfig, MeshOSConfig, MeshOSOptions, WorkflowConfig, WorkerConfig, WorkerOptions, WorkflowSearchOptions, 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';
package/modules/utils.ts CHANGED
@@ -8,6 +8,11 @@ export async function sleepFor(ms: number) {
8
8
  return new Promise((resolve) => setTimeout(resolve, ms));
9
9
  }
10
10
 
11
+ export function deterministicRandom(seed: number): number {
12
+ let x = Math.sin(seed) * 10000;
13
+ return x - Math.floor(x);
14
+ }
15
+
11
16
  export function identifyRedisType(redisInstance: any): 'redis' | 'ioredis' | null {
12
17
  const prototype = Object.getPrototypeOf(redisInstance);
13
18
  if ('defineCommand' in prototype || Object.keys(prototype).includes('multi')) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.24",
3
+ "version": "0.0.26",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -1,20 +1,29 @@
1
1
  import { nanoid } from 'nanoid';
2
2
 
3
+ import { Durable } from '.';
4
+ import { asyncLocalStorage } from './asyncLocalStorage';
3
5
  import { ClientService as Client } from './client';
4
6
  import { WorkflowHandleService } from './handle';
5
7
  import { Search } from './search';
6
8
  import { WorkerService as Worker } from './worker';
7
- import { FindOptions, MeshOSActivityOptions, MeshOSOptions, WorkflowSearchOptions } from '../../types/durable';
8
- import { RedisOptions, RedisClass } from '../../types/redis';
9
- import { StringAnyType } from '../../types';
10
- import { Durable } from '.';
11
- import { asyncLocalStorage } from './asyncLocalStorage';
12
9
  import { WorkflowService } from './workflow';
10
+ import { StreamSignaler } from '../signaler/stream';
11
+ import {
12
+ FindOptions,
13
+ FindWhereOptions,
14
+ FindWhereQuery,
15
+ MeshOSActivityOptions,
16
+ MeshOSConfig,
17
+ MeshOSOptions,
18
+ MeshOSWorkerOptions,
19
+ WorkflowSearchOptions } from '../../types/durable';
20
+ import { RedisOptions, RedisClass } from '../../types/redis';
21
+ import { StringAnyType } from '../../types/serializer';
13
22
 
14
23
  /**
15
24
  * The base class for running MeshOS workflows.
16
- * Extend and register subclass methods by name to
17
- * execute as durable workflows, backed by Redis.
25
+ * Extend this class, add your Redis config, and add functions to
26
+ * execute as durable `hooks`, `workflows`, and `activities`.
18
27
  */
19
28
 
20
29
  export class MeshOSService {
@@ -122,14 +131,25 @@ export class MeshOSService {
122
131
  }
123
132
 
124
133
  /**
125
- * Initialize the worker(s) for the entity. This is a static
134
+ * stop the workers
135
+ * @returns {Promise<void>}
136
+ */
137
+ static async stopWorkers(): Promise<void> {
138
+ await Durable.Client.shutdown();
139
+ await Durable.Worker.shutdown();
140
+ await StreamSignaler.stopConsuming();
141
+ }
142
+
143
+ /**
144
+ * Initializes the worker(s). This is a static
126
145
  * method that allows for optional task Queue targeting.
127
- * NOTE: Allow List may be optionally used to only wrap
128
- * specific methods in this class.
129
- * @param {string} taskQueue
130
- * @param {string[]} allowList
146
+ * An `allowList` may be optionally provided to start
147
+ * specific `worker` and `hook` methods.
148
+ * @param {MeshOSWorkerOptions} [options]
131
149
  */
132
- static async startWorkers(taskQueue?: string, allowList: Array<MeshOSOptions | string> = []) {
150
+ static async startWorkers(options?: MeshOSWorkerOptions) {
151
+ const taskQueue = options && options.taskQueue;
152
+ const allowList = options && options.allowList || [];
133
153
  const my = new this();
134
154
 
135
155
  //helper functions
@@ -156,8 +176,6 @@ export class MeshOSService {
156
176
  const proxiedActivities = Durable.workflow.proxyActivities({
157
177
  activities: proxyActivities
158
178
  });
159
- //WATCH!: unsure if this will pollute the scope; don't think
160
- // so as activity functions are terminal in the chain.
161
179
  Object.assign(my, proxiedActivities);
162
180
  }
163
181
 
@@ -229,9 +247,9 @@ export class MeshOSService {
229
247
  class: my.redisClass,
230
248
  options: my.redisOptions
231
249
  }});
232
- //workflow name is the function name driving the workflow
250
+
233
251
  let workflowName: string;
234
- if (options?.workflowName) {
252
+ if (options.workflowName) {
235
253
  workflowName = options?.workflowName
236
254
  } else if(my.workflowFunctions?.length) {
237
255
  let target = my.workflowFunctions[0];
@@ -242,12 +260,70 @@ export class MeshOSService {
242
260
  }
243
261
  }
244
262
  return await client.workflow.search(
245
- options?.taskQueue ?? my.taskQueue,
263
+ options.taskQueue ?? my.taskQueue,
246
264
  workflowName,
247
- my.namespace,
248
- my.search.index,
265
+ options.namespace ?? my.namespace,
266
+ options.index ?? my.search.index,
249
267
  ...args,
250
- ); //[count, [id, fields[], id, fields[], id, fields[], ...]]
268
+ ); //[count, [id, fields[]], [id, fields[]], [id, fields[]], ...]]
269
+ }
270
+
271
+ /**
272
+ * Provides a JSON abstraction for the Redis FT.search command
273
+ * (e.g, `count`, `query`, `return`, `limit`)
274
+ * @param {FindWhereOptions} options
275
+ * @returns {Promise<string[] | [number]>}
276
+ */
277
+ static async findWhere(options: FindWhereOptions): Promise<string[] | [number]> {
278
+ const args: string[] = [this.generateSearchQuery(options.query)];
279
+ if (options.count) {
280
+ args.push('LIMIT', '0', '0');
281
+ } else {
282
+ //limit which hash fields to return
283
+ if (options.return?.length) {
284
+ args.push('RETURN');
285
+ args.push(options.return.length.toString());
286
+ options.return.forEach(returnField => {
287
+ args.push(`_${returnField}`);
288
+ });
289
+ }
290
+ //paginate
291
+ if (options.limit) {
292
+ args.push('LIMIT', options.limit.start.toString(), options.limit.size.toString());
293
+ }
294
+ }
295
+ return await this.find(options.options ?? {}, ...args);
296
+ }
297
+
298
+ static generateSearchQuery(query: FindWhereQuery[]) {
299
+ const my = new this();
300
+ let queryString = query.map(q => {
301
+ const { field, is, value, type } = q;
302
+ const prefixedFieldName = my.search?.schema && field in my.search.schema ? `@_${field}` : `@${field}`;
303
+ const fieldType = my.search?.schema[field]?.type ?? type ?? 'TEXT';
304
+
305
+ switch (fieldType) {
306
+ case 'TAG':
307
+ return `${prefixedFieldName}:{${value}}`;
308
+ case 'TEXT':
309
+ return `${prefixedFieldName}:"${value}"`;
310
+ case 'NUMERIC':
311
+ let range = '';
312
+ if (is.startsWith('=')) {
313
+ range = `[${value} ${value}]`;
314
+ } else if (is === '<') {
315
+ range = `[-inf ${value}]`;
316
+ } else if (is === '>') {
317
+ range = `[${value} +inf]`;
318
+ } else if (is === '[]') {
319
+ range = `[${value[0]} ${value[1]}]`
320
+ }
321
+ return `${prefixedFieldName}:${range}`;
322
+ default:
323
+ return '';
324
+ }
325
+ }).join(' ');
326
+ return queryString;
251
327
  }
252
328
 
253
329
  /**
@@ -281,8 +357,14 @@ export class MeshOSService {
281
357
  * Optionally include a target taskQueue to exec the
282
358
  * workflow's call on a specific worker queue.
283
359
  */
284
- constructor(id?: string, options?: Record<string, any>) {
285
- this.id = id;
360
+ constructor(id?: string | MeshOSConfig, options?: MeshOSConfig) {
361
+ if (typeof(id) === 'string') {
362
+ this.id = id;
363
+ } else if (id?.id) {
364
+ this.id = id.id;
365
+ options = id;
366
+ id = undefined;
367
+ };
286
368
  if (options?.taskQueue) {
287
369
  this.taskQueue = options.taskQueue;
288
370
  } else if (!id && !options?.taskQueue) {
@@ -19,6 +19,7 @@ import {
19
19
  WorkflowOptions } from "../../types/durable";
20
20
  import { JobOutput, JobState } from '../../types/job';
21
21
  import { StreamStatus } from '../../types/stream';
22
+ import { deterministicRandom } from '../../modules/utils';
22
23
 
23
24
  export class WorkflowService {
24
25
 
@@ -173,6 +174,19 @@ export class WorkflowService {
173
174
  return guidValue === 1;
174
175
  }
175
176
 
177
+ /**
178
+ * returns a random number between 0 and 1. This number is deterministic
179
+ * and will never vary for a given seed. This is useful for randomizing
180
+ * pathways in a workflow that can be safely replayed.
181
+ * @returns {number}
182
+ */
183
+ static random(): number {
184
+ const store = asyncLocalStorage.getStore();
185
+ const COUNTER = store.get('counter');
186
+ const seed = COUNTER.counter = COUNTER.counter + 1;
187
+ return deterministicRandom(seed);
188
+ }
189
+
176
190
  /**
177
191
  * send signal data into any other paused thread (which is paused and
178
192
  * awaiting the signal) from within a hook-thread or the main-thread
@@ -46,8 +46,7 @@ class RedisStoreService extends StoreService<RedisClientType, RedisMultiType> {
46
46
  }
47
47
 
48
48
  getMulti(): RedisMultiType {
49
- const multi = this.redisClient.MULTI();
50
- return multi as unknown as RedisMultiType;
49
+ return this.redisClient.multi() as unknown as RedisMultiType;
51
50
  }
52
51
 
53
52
  async exec(...args: any[]): Promise<string|string[]|string[][]> {
@@ -21,7 +21,7 @@ class RedisStreamService extends StreamService<RedisClientType, RedisMultiType>
21
21
  }
22
22
 
23
23
  getMulti(): RedisMultiType {
24
- return this.redisClient.MULTI() as unknown as RedisMultiType;
24
+ return this.redisClient.multi() as unknown as RedisMultiType;
25
25
  }
26
26
 
27
27
  mintKey(type: KeyType, params: KeyStoreParams): string {
@@ -21,7 +21,7 @@ class RedisSubService extends SubService<RedisClientType, RedisMultiType> {
21
21
  }
22
22
 
23
23
  getMulti(): RedisMultiType {
24
- const multi = this.redisClient.MULTI();
24
+ const multi = this.redisClient.multi();
25
25
  return multi as unknown as RedisMultiType;
26
26
  }
27
27
 
package/types/durable.ts CHANGED
@@ -65,12 +65,9 @@ type MeshOSClassConfig = {
65
65
  }
66
66
 
67
67
  type MeshOSConfig = {
68
- taskQueue?: string;
69
- index?: {
70
- index: string;
71
- prefix: string[];
72
- schema: Record<string, {type: 'TEXT' | 'NUMERIC' | 'TAG', sortable: boolean}>;
73
- };
68
+ id?: string; //guid for the workflow when instancing
69
+ await?: boolean; //default is false; must explicitly send true to await the final result
70
+ taskQueue?: string; //optional target queue isolate for the function
74
71
  }
75
72
 
76
73
  type ConnectionConfig = {
@@ -96,6 +93,13 @@ type WorkerConfig = {
96
93
  search?: WorkflowSearchOptions;
97
94
  }
98
95
 
96
+ type FindWhereQuery = {
97
+ field: string;
98
+ is: string;
99
+ value: string | boolean | number | [number, number];
100
+ type?: string; //default is TEXT
101
+ }
102
+
99
103
  type FindOptions = {
100
104
  workflowName?: string; //also the function name
101
105
  taskQueue?: string;
@@ -103,6 +107,17 @@ type FindOptions = {
103
107
  index?: string; //the FT search index name
104
108
  }
105
109
 
110
+ type FindWhereOptions = {
111
+ options?: FindOptions;
112
+ count?: boolean;
113
+ query: FindWhereQuery[];
114
+ return?: string[];
115
+ limit?: {
116
+ start: number,
117
+ size: number
118
+ }
119
+ }
120
+
106
121
  type MeshOSOptions = {
107
122
  name: string;
108
123
  options: WorkerOptions;
@@ -113,6 +128,14 @@ type MeshOSActivityOptions = {
113
128
  options: ActivityConfig;
114
129
  }
115
130
 
131
+ type MeshOSWorkerOptions = {
132
+ taskQueue?: string; //change the default task queue
133
+ allowList?: Array<MeshOSOptions | string>; //limit which `hook` and `workflow` workers start
134
+ logLevel?: string; //debug, info, warn, error
135
+ maxSystemRetries?: number; //1-3 (10ms, 100ms, 1_000ms)
136
+ backoffCoefficient?: number; //2-10ish
137
+ }
138
+
116
139
  type WorkerOptions = {
117
140
  logLevel?: string; //debug, info, warn, error
118
141
  maxSystemRetries?: number; //1-3 (10ms, 100ms, 1_000ms)
@@ -151,8 +174,11 @@ export {
151
174
  Registry,
152
175
  SignalOptions,
153
176
  FindOptions,
177
+ FindWhereOptions,
178
+ FindWhereQuery,
154
179
  HookOptions,
155
180
  MeshOSActivityOptions,
181
+ MeshOSWorkerOptions,
156
182
  MeshOSClassConfig,
157
183
  MeshOSConfig,
158
184
  MeshOSOptions,
package/types/index.ts CHANGED
@@ -38,8 +38,11 @@ export {
38
38
  Registry,
39
39
  SignalOptions,
40
40
  FindOptions,
41
+ FindWhereOptions,
42
+ FindWhereQuery,
41
43
  HookOptions,
42
44
  MeshOSActivityOptions,
45
+ MeshOSWorkerOptions,
43
46
  MeshOSClassConfig,
44
47
  MeshOSConfig,
45
48
  MeshOSOptions,
@@ -49,7 +52,7 @@ export {
49
52
  WorkflowSearchOptions,
50
53
  WorkflowDataType,
51
54
  WorkflowOptions,
52
- }from './durable'
55
+ } from './durable'
53
56
  export {
54
57
  HookCondition,
55
58
  HookConditions,