@hotmeshio/hotmesh 0.0.23 → 0.0.24

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 (37) hide show
  1. package/README.md +66 -67
  2. package/build/index.d.ts +2 -1
  3. package/build/index.js +3 -1
  4. package/build/package.json +2 -2
  5. package/build/services/durable/factory.js +6 -6
  6. package/build/services/durable/handle.js +2 -4
  7. package/build/services/durable/index.d.ts +2 -2
  8. package/build/services/durable/index.js +2 -2
  9. package/build/services/durable/meshos.d.ts +108 -0
  10. package/build/services/durable/meshos.js +289 -0
  11. package/build/services/durable/search.js +0 -1
  12. package/build/services/durable/worker.d.ts +1 -1
  13. package/build/services/durable/worker.js +8 -4
  14. package/build/services/durable/workflow.d.ts +4 -0
  15. package/build/services/durable/workflow.js +21 -9
  16. package/build/services/signaler/stream.js +1 -2
  17. package/build/services/store/clients/ioredis.js +2 -2
  18. package/build/services/store/clients/redis.js +1 -1
  19. package/build/types/durable.d.ts +19 -5
  20. package/build/types/index.d.ts +1 -1
  21. package/index.ts +2 -1
  22. package/package.json +2 -2
  23. package/services/durable/factory.ts +6 -6
  24. package/services/durable/handle.ts +2 -4
  25. package/services/durable/index.ts +2 -2
  26. package/services/durable/meshos.ts +344 -0
  27. package/services/durable/search.ts +0 -1
  28. package/services/durable/worker.ts +8 -5
  29. package/services/durable/workflow.ts +23 -10
  30. package/services/signaler/stream.ts +1 -2
  31. package/services/store/clients/ioredis.ts +2 -3
  32. package/services/store/clients/redis.ts +1 -1
  33. package/types/durable.ts +26 -6
  34. package/types/index.ts +6 -2
  35. package/build/services/durable/meshdb.d.ts +0 -113
  36. package/build/services/durable/meshdb.js +0 -211
  37. package/services/durable/meshdb.ts +0 -254
package/README.md CHANGED
@@ -3,6 +3,13 @@
3
3
 
4
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.
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*.
8
+
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.
12
+
6
13
  ## Install
7
14
  [![npm version](https://badge.fury.io/js/%40hotmeshio%2Fhotmesh.svg)](https://badge.fury.io/js/%40hotmeshio%2Fhotmesh)
8
15
 
@@ -11,92 +18,84 @@ npm install @hotmeshio/hotmesh
11
18
  ```
12
19
 
13
20
  ## Design
14
- The HotMesh SDK is designed to keep your code front-and-center. Write functions as you normally would, then use HotMesh to make them durable.
21
+ 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.
15
22
 
16
- 1. Start by defining **activities**. Activities can be written in any style, using any framework, and can even be legacy functions you've already written. The only requirement is that they return a Promise. *Note how the `saludar` example throws an error 50% of the time. It doesn't matter how unpredictable your functions are, HotMesh will retry as necessary until they succeed.*
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.*
17
24
  ```javascript
18
- //activities.ts
25
+ //myworkflow.ts
19
26
 
20
- export async function greet(name: string): Promise<string> {
21
- return `Hello, ${name}!`;
22
- }
27
+ export class MyWorkflow {
23
28
 
24
- export async function saludar(nombre: string): Promise<string> {
25
- if (Math.random() > 0.5) throw new Error('Random error');
26
- return `¡Hola, ${nombre}!`;
27
- }
28
- ```
29
- 2. Define your **workflow** logic. Include conditional branching, loops, etc to control activity execution. It's vanilla code written in your own coding style. The only requirement is to use `proxyActivities`, ensuring your activities are executed with HotMesh's durability guarantee.
30
- ```javascript
31
- //workflows.ts
32
-
33
- import { Durable } from '@hotmeshio/hotmesh';
34
- import * as activities from './activities';
29
+ //main method
30
+ async run(name: string): Promise<string> {
31
+ const salud = await this.saludar(name);
32
+ const hello = await this.greet(name);
33
+ return `${hello} ${salud}`;
34
+ }
35
35
 
36
- const { greet, saludar } = Durable.workflow
37
- .proxyActivities<typeof activities>({
38
- activities
39
- });
36
+ //this function only succeeds 50% of the time!
37
+ async saludar(nombre: string): Promise<string> {
38
+ if (Math.random() < 0.5) {
39
+ throw new Error('¡No hablo español!');
40
+ }
41
+ return `¡Hola, ${nombre}!`;
42
+ }
40
43
 
41
- export async function example(name: string, lang: string): Promise<string> {
42
- if (lang === 'es') {
43
- return await saludar(name);
44
- } else {
45
- return await greet(name);
44
+ //this function always succeeds
45
+ async greet(name: string): Promise<string> {
46
+ return `Hello, ${name}!`;
46
47
  }
47
48
  }
48
49
  ```
49
-
50
- 3. Although you could call your workflow directly (it's just a vanilla function), it's only durable when invoked and orchestrated via HotMesh.
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).*
51
51
  ```javascript
52
- //client.ts
52
+ //myworkflow.ts
53
53
 
54
- import { Durable } from '@hotmeshio/hotmesh';
55
54
  import Redis from 'ioredis'; //OR `import * as Redis from 'redis';`
56
- import { nanoid } from 'nanoid';
55
+ import { MeshOS } from '@hotmeshio/hotmesh';
57
56
 
58
- async function run(): Promise<string> {
59
- const client = new Durable.Client({
60
- connection: {
61
- class: Redis,
62
- options: { host: 'localhost', port: 6379 }
63
- }
64
- });
57
+ export class MyWorkflow extends MeshOS {
65
58
 
66
- const handle = await client.workflow.start({
67
- args: ['HotMesh', 'es'],
68
- taskQueue: 'hello-world',
69
- workflowName: 'example',
70
- workflowId: nanoid()
71
- });
59
+ redisClass = Redis;
60
+ redisOptions = { host: 'localhost', port: 6379 };
72
61
 
73
- return await handle.result();
74
- //returns '¡Hola, HotMesh!'
75
- }
76
- ```
62
+ //list functions to run as durable workflows
63
+ workflowFunctions = ['run'];
77
64
 
78
- 4. Finally, create a **worker** and link your workflow function. Workers listen for tasks on their assigned Redis stream and invoke your workflow function each time they receive an event.
79
- ```javascript
80
- //worker.ts
81
-
82
- import { Durable } from '@hotmeshio/hotmesh';
83
- import Redis from 'ioredis';
84
- import * as workflows from './workflows';
85
-
86
- async function run() {
87
- const worker = await Durable.Worker.create({
88
- connection: {
89
- class: Redis,
90
- options: { host: 'localhost', port: 6379 },
91
- },
92
- taskQueue: 'hello-world',
93
- workflow: workflows.example,
94
- });
95
- await worker.run();
65
+ //list functions to retry and cache
66
+ proxyFunctions = ['saludar'];
67
+
68
+ //main method (Redis will now govern its execution)
69
+ async run(name: string): Promise<string> {
70
+ const salud = await this.saludar(name);
71
+ const hello = await this.greet(name);
72
+ return `${hello} ${salud}`;
73
+ }
74
+
75
+ //this function will now be retried until it succeeds;
76
+ async saludar(nombre: string): Promise<string> {
77
+ if (Math.random() < 0.5) {
78
+ throw new Error('¡No hablo español!');
79
+ }
80
+ return `¡Hola, ${nombre}!`;
81
+ }
82
+
83
+ //this vanilla function is unchanged
84
+ async greet(name: string): Promise<string> {
85
+ return `Hello, ${name}!`;
86
+ }
96
87
  }
97
88
  ```
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.
90
+ ```javascript
91
+ //mycaller.ts
92
+
93
+ //...import MyWorkflow, etc...
98
94
 
99
- >HotMesh delivers durable workflows without the cost and complexity of a centralized service mesh. Refer to the [samples-typescript](https://github.com/hotmeshio/samples-typescript) Git Repo for a range of examples, including compositional workflows (where one workflow calls another) and remote execution (where calls are brokered across microservices).
95
+ const workflow = new MyWorkflow('unique123', { await: true }); //await the response
96
+ const response = await workflow.run('John');
97
+ //Hello, John! ¡Hola, John!
98
+ ```
100
99
 
101
100
  ## Advanced Design
102
101
  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.
package/build/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Durable } from './services/durable';
2
+ import { MeshOSService as MeshOS } from './services/durable/meshos';
2
3
  import { HotMeshService as HotMesh } from './services/hotmesh';
3
4
  import { HotMeshConfig } from './types/hotmesh';
4
- export { Durable, HotMesh, HotMeshConfig };
5
+ export { Durable, HotMesh, HotMeshConfig, MeshOS };
package/build/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HotMesh = exports.Durable = void 0;
3
+ exports.MeshOS = exports.HotMesh = exports.Durable = void 0;
4
4
  const durable_1 = require("./services/durable");
5
5
  Object.defineProperty(exports, "Durable", { enumerable: true, get: function () { return durable_1.Durable; } });
6
+ const meshos_1 = require("./services/durable/meshos");
7
+ Object.defineProperty(exports, "MeshOS", { enumerable: true, get: function () { return meshos_1.MeshOSService; } });
6
8
  const hotmesh_1 = require("./services/hotmesh");
7
9
  Object.defineProperty(exports, "HotMesh", { enumerable: true, get: function () { return hotmesh_1.HotMeshService; } });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.23",
3
+ "version": "0.0.24",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -43,7 +43,7 @@
43
43
  "test:sub:redis": "NODE_ENV=test jest ./tests/functional/sub/clients/redis.test.ts --detectOpenHandles --forceExit --verbose",
44
44
  "test:sub:ioredis": "NODE_ENV=test jest ./tests/functional/sub/clients/ioredis.test.ts --detectOpenHandles --forceExit --verbose",
45
45
  "test:durable": "NODE_ENV=test jest ./tests/durable/*/index.test.ts --detectOpenHandles --forceExit --verbose",
46
- "test:durable:meshdb": "NODE_ENV=test jest ./tests/durable/meshdb/index.test.ts --detectOpenHandles --forceExit --verbose",
46
+ "test:durable:meshos": "NODE_ENV=test jest ./tests/durable/meshos/index.test.ts --detectOpenHandles --forceExit --verbose",
47
47
  "test:durable:hello": "NODE_ENV=test jest ./tests/durable/helloworld/index.test.ts --detectOpenHandles --forceExit --verbose",
48
48
  "test:durable:goodbye": "NODE_ENV=test jest ./tests/durable/goodbye/index.test.ts --detectOpenHandles --forceExit --verbose",
49
49
  "test:durable:hook": "NODE_ENV=test jest ./tests/durable/hook/index.test.ts --detectOpenHandles --forceExit --verbose",
@@ -267,11 +267,11 @@ const getWorkflowYAML = (app, version) => {
267
267
  - ['{@string.concat}']
268
268
  cycleWorkflowId:
269
269
  '@pipe':
270
- - ['{$job.metadata.jid}', '-$wfc', '{sig.output.metadata.dad}', '-', '{sigw1.output.data.index}']
270
+ - ['-', '{$job.metadata.jid}', '-$wfc', '{sig.output.metadata.dad}', '-', '{sigw1.output.data.index}']
271
271
  - ['{@string.concat}']
272
272
  baseWorkflowId:
273
273
  '@pipe':
274
- - ['{$job.metadata.jid}', '-$wfs', '{sig.output.metadata.dad}', '-']
274
+ - ['-', '{$job.metadata.jid}', '-$wfs', '{sig.output.metadata.dad}', '-']
275
275
  - ['{@string.concat}']
276
276
  output:
277
277
  schema:
@@ -315,7 +315,7 @@ const getWorkflowYAML = (app, version) => {
315
315
  - ['{@string.concat}']
316
316
  workflowId:
317
317
  '@pipe':
318
- - ['{$job.metadata.jid}', '-$sleep', '{sig.output.metadata.dad}', '-', '{sigw1.output.data.index}']
318
+ - ['-', '{$job.metadata.jid}', '-$sleep', '{sig.output.metadata.dad}', '-', '{sigw1.output.data.index}']
319
319
  - ['{@string.concat}']
320
320
  output:
321
321
  schema:
@@ -385,11 +385,11 @@ const getWorkflowYAML = (app, version) => {
385
385
  - ['{@string.concat}']
386
386
  cycleWorkflowId:
387
387
  '@pipe':
388
- - ['{$job.metadata.jid}', '-$wfc-', '{w1.output.data.index}']
388
+ - ['-', '{$job.metadata.jid}', '-$wfc-', '{w1.output.data.index}']
389
389
  - ['{@string.concat}']
390
390
  baseWorkflowId:
391
391
  '@pipe':
392
- - ['{$job.metadata.jid}', '-$wfs-']
392
+ - ['-', '{$job.metadata.jid}', '-$wfs-']
393
393
  - ['{@string.concat}']
394
394
  output:
395
395
  schema:
@@ -433,7 +433,7 @@ const getWorkflowYAML = (app, version) => {
433
433
  - ['{@string.concat}']
434
434
  workflowId:
435
435
  '@pipe':
436
- - ['{$job.metadata.jid}', '-$sleep-', '{w1.output.data.index}']
436
+ - ['-', '{$job.metadata.jid}', '-$sleep-', '{w1.output.data.index}']
437
437
  - ['{@string.concat}']
438
438
  output:
439
439
  schema:
@@ -30,10 +30,8 @@ class WorkflowHandleService {
30
30
  throw new Error(JSON.parse(state.metadata.err));
31
31
  }
32
32
  if (state?.data?.done) {
33
- //child flows are never technically 'done' as they have an open hook
34
- //that is tied to the parent flow's completion. so, we need to check
35
- //the 'done' flag on the child flow's payload (not the 'js' metadata field
36
- //which is typically used); the `loadState` parameter triggers this
33
+ //child flows are never 'done'; they use a hook
34
+ //that only closes upon parent flow completion.
37
35
  return state.data.response;
38
36
  }
39
37
  }
@@ -1,13 +1,13 @@
1
1
  import { ClientService } from './client';
2
2
  import { ConnectionService } from './connection';
3
- import { MeshDBService } from './meshdb';
3
+ import { MeshOSService } from './meshos';
4
4
  import { WorkerService } from './worker';
5
5
  import { WorkflowService } from './workflow';
6
6
  import { ContextType } from '../../types/durable';
7
7
  export declare const Durable: {
8
8
  Client: typeof ClientService;
9
9
  Connection: typeof ConnectionService;
10
- MeshDB: typeof MeshDBService;
10
+ MeshOS: typeof MeshOSService;
11
11
  Worker: typeof WorkerService;
12
12
  workflow: typeof WorkflowService;
13
13
  };
@@ -3,13 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Durable = void 0;
4
4
  const client_1 = require("./client");
5
5
  const connection_1 = require("./connection");
6
- const meshdb_1 = require("./meshdb");
6
+ const meshos_1 = require("./meshos");
7
7
  const worker_1 = require("./worker");
8
8
  const workflow_1 = require("./workflow");
9
9
  exports.Durable = {
10
10
  Client: client_1.ClientService,
11
11
  Connection: connection_1.ConnectionService,
12
- MeshDB: meshdb_1.MeshDBService,
12
+ MeshOS: meshos_1.MeshOSService,
13
13
  Worker: worker_1.WorkerService,
14
14
  workflow: workflow_1.WorkflowService,
15
15
  };
@@ -0,0 +1,108 @@
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
+ import { WorkflowService } from './workflow';
6
+ /**
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.
10
+ */
11
+ export declare class MeshOSService {
12
+ /**
13
+ * The top-level Redis isolation. All workflow data is
14
+ * isolated within this namespace. Values should be
15
+ * lower-case with no spaces (e.g, 'staging', 'prod', 'test',
16
+ * 'routing-stagig', 'reporting-prod', etc.).
17
+ * 1) only url-safe values are allowed;
18
+ * 2) the 'a' symbol is reserved by HotMesh for indexing apps
19
+ */
20
+ namespace: string;
21
+ /**
22
+ * Data is routed to workers that specify this task queue.
23
+ * Setting the task queue when the worker is created will
24
+ * ensure that the worker only receives messages destined
25
+ * for the queue. Callers can specify the taskQue to when
26
+ * starting a job to call those workers.
27
+ */
28
+ taskQueue: string;
29
+ /**
30
+ * These methods run as durable workflows
31
+ */
32
+ workflowFunctions: Array<MeshOSOptions | string>;
33
+ /**
34
+ * These methods run as hooks (hook into a running workflow)
35
+ */
36
+ hookFunctions: Array<MeshOSOptions | string>;
37
+ /**
38
+ * These methods run as proxied activities (and are safely memoized)
39
+ */
40
+ proxyFunctions: Array<MeshOSActivityOptions | string>;
41
+ /**
42
+ * The workflow GUID. Workflows will be persisted to
43
+ * Redis using the pattern hmsh:<namespace>:j:<id>.
44
+ */
45
+ id: string;
46
+ /**
47
+ * The Redis connection options. NOTE: Redis and IORedis
48
+ * use different formats for their connection config.
49
+ */
50
+ redisOptions: RedisOptions;
51
+ /**
52
+ * The Redis connection class.
53
+ *
54
+ * @example
55
+ * import Redis from 'ioredis';
56
+ * import * as Redis from 'redis';
57
+ */
58
+ redisClass: RedisClass;
59
+ /**
60
+ * Optional model declaration (custom workflow state)
61
+ */
62
+ model: StringAnyType;
63
+ /**
64
+ * Optional configuration for Redis FT search
65
+ */
66
+ search: WorkflowSearchOptions;
67
+ static MeshOS: typeof WorkflowService;
68
+ static getHotMeshClient(redisClass: RedisClass, redisOptions: RedisOptions, namespace: string, taskQueue: string): Promise<import("../hotmesh").HotMeshService>;
69
+ /**
70
+ * mints a workflow ID, using the search prefix.
71
+ * NOTE: The prefix is necesary when indexing
72
+ * HASHes when FT search is enabled.
73
+ * @returns {string}
74
+ */
75
+ static mintGuid(): string;
76
+ /**
77
+ * Creates an FT search index
78
+ */
79
+ static createIndex(): Promise<void>;
80
+ /**
81
+ * Initialize the worker(s) for the entity. This is a static
82
+ * 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
87
+ */
88
+ static startWorkers(taskQueue?: string, allowList?: Array<MeshOSOptions | string>): Promise<void>;
89
+ /**
90
+ * executes the redis FT search query
91
+ * @example '@_quantity:[89 89]'
92
+ * @param {any[]} args
93
+ * @returns {string}
94
+ */
95
+ static find(options: FindOptions, ...args: string[]): Promise<string[] | [number]>;
96
+ /**
97
+ * returns the workflow handle. The handle can then be
98
+ * used to query for status, state, custom state, etc.
99
+ * @param {string} id
100
+ * @returns {Promise<WorkflowHandleService>}
101
+ */
102
+ static get(id: string): Promise<WorkflowHandleService>;
103
+ /**
104
+ * Optionally include a target taskQueue to exec the
105
+ * workflow's call on a specific worker queue.
106
+ */
107
+ constructor(id?: string, options?: Record<string, any>);
108
+ }