@hotmeshio/hotmesh 0.0.29 → 0.0.31

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
@@ -92,19 +92,25 @@ npm install @hotmeshio/hotmesh
92
92
 
93
93
  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. The `MeshOS` base class (shown in the examples above) provides additional methods for solving the most common state management challenges.
94
94
 
95
- - `waitForSignal` | Pause your function 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.
95
+ - `waitForSignal` | Pause your function and wait for external event(s) before continuing. The *waitForSignal* method will collate and cache the signals and only awaken your function once all signals have arrived.
96
96
  - `signal` | Send a signal (and optional payload) to any paused function.
97
97
  - `hook` | Redis governance converts your functions into 're-entrant processes'. Optionally use the *hook* method to spawn parallel execution threads to augment a running workflow.
98
- - `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.
98
+ - `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 and your function will resume right where it left off.
99
99
  - `random` | Generate a deterministic random number that can be used in a reentrant process workflow (replaces `Math.random()`).
100
100
  - `executeChild` | Call another durable function and await the response. *Design sophisticated, multi-process solutions by leveraging this command.*
101
101
  - `startChild` | Call another durable function, but do not await the response.
102
- - `set` | Set one or more name/value pairs (e.g, `set('name1', 'value1', 'name1', 'value2')`)
103
- - `get` | Get a single value by name(e.g, `get('name')`)
104
- - `mget` | Get multiple values by name (e.g, `get('name1', 'name2')`)
105
- - `del` | Delete one or more entries by name and return the number deleted (e.g, `del('name1', 'name1')`)
106
- - `incr` | Increment (or decrement) a number (e.g, `incr('name', -99)`)
107
- - `mult` | Multiply a number (e.g, `mult('name', 12)`)
102
+ - `search` | Instance a search session (e.g, `const search = MeshOS.search()`)
103
+ - `set` | Set one or more name/value pairs (e.g, `search.set('name1', 'value1', 'name2', 'value2')`)
104
+ - `get` | Get a single value by name(e.g, `search.get('name')`)
105
+ - `mget` | Get multiple values by name (e.g, `search.mget('name1', 'name2')`)
106
+ - `del` | Delete one or more entries by name and return the number deleted (e.g, `search.del('name1', 'name2')`)
107
+ - `incr` | Increment (or decrement) a number (e.g, `search.incr('name', -99)`)
108
+ - `mult` | Multiply a number (e.g, `search.mult('name', 12)`)
109
+ - `find` | Find workflows using the native Redis [FT.*](https://redis.io/commands/ft.search/) search commands
110
+ - `findWhere` | Find workflows using a simplified, JSON-based search syntax that overlays the native Redis FT.SEARCH syntax.
111
+ - `createIndex` | Create a searchable index in Redis using simplified, JSON-based syntax that overlays the native Redis FT.CREATE syntax.
112
+ - `startWorkers` | Start the workers necessary to govern your class (typically called at server startup).
113
+ - `stopWorkers` | Stop all workers (typically called at server shutdown)
108
114
 
109
115
  Refer to the [hotmeshio/samples-typescript](https://github.com/hotmeshio/samples-typescript) repo for usage examples.
110
116
 
@@ -216,31 +222,31 @@ const hotMesh = await HotMesh.init({
216
222
  ```
217
223
 
218
224
  ### Observability
219
- Workflows and activities are run according to the rules you define, offering [Graph-Oriented](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/system_lifecycle.md#telemetry) telemetry insights into your legacy function executions.
225
+ Workflows and activities are run according to the rules you define, offering [Graph-Oriented](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/system_lifecycle.md#telemetry) telemetry insights into your legacy function executions.
220
226
 
221
227
  ## FAQ
222
- Refer to the [FAQ](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/faq.md) for terminology, definitions, and an exploration of how HotMesh facilitates orchestration use cases.
228
+ 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.
223
229
 
224
230
  ## Quick Start
225
- Refer to the [Quick Start](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/quickstart.md) for sample flows you can easily copy, paste, and modify to get started.
231
+ Refer to the [Quick Start](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/quickstart.md) for sample flows you can easily copy, paste, and modify to get started.
226
232
 
227
233
  ## Developer Guide
228
- 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/blob/main/docs/developer_guide.md).
234
+ 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).
229
235
 
230
236
  ## Model Driven Development
231
- [Model Driven Development](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/model_driven_development.md) is an established strategy for managing process-oriented tasks. Check out this guide to understand its foundational principles.
237
+ [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.
232
238
 
233
239
  ## Data Mapping
234
- 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/blob/main/docs/data_mapping.md).
240
+ 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).
235
241
 
236
242
  ## Composition
237
- 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/blob/main/docs/composable_workflow.md).
243
+ 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).
238
244
 
239
245
  ## Distributed Orchestration
240
- HotMesh is a distributed orchestration engine. Refer to the [Distributed Orchestration Guide](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/distributed_orchestration.md) for a detailed breakdown of the approach.
246
+ 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 detailed breakdown of the approach.
241
247
 
242
248
  ## System Lifecycle
243
- 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).
249
+ 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).
244
250
 
245
251
  ## Alpha Release
246
- So what exacty is an [alpha release](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/alpha.md)?
252
+ So what exacty is an [alpha release](https://github.com/hotmeshio/sdk-typescript/tree/main/docs/alpha.md)?
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.29",
3
+ "version": "0.0.31",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -54,7 +54,7 @@
54
54
  "test:durable:loopactivity": "NODE_ENV=test jest ./tests/durable/loopactivity/index.test.ts --detectOpenHandles --forceExit --verbose",
55
55
  "test:durable:nested": "NODE_ENV=test jest ./tests/durable/nested/index.test.ts --detectOpenHandles --forceExit --verbose"
56
56
  },
57
- "keywords": [],
57
+ "keywords": ["durable workflow", "hotmesh", "service mesh", "workflows", "operational data", "redis"],
58
58
  "author": "luke.birdeau@gmail.com",
59
59
  "license": "SEE LICENSE IN LICENSE",
60
60
  "dependencies": {
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ClientService = void 0;
4
- const nanoid_1 = require("nanoid");
5
4
  const factory_1 = require("./factory");
6
5
  const handle_1 = require("./handle");
7
6
  const hotmesh_1 = require("../hotmesh");
@@ -88,14 +87,14 @@ class ClientService {
88
87
  const payload = {
89
88
  arguments: [...options.args],
90
89
  parentWorkflowId: options.parentWorkflowId,
91
- workflowId: options.workflowId || (0, nanoid_1.nanoid)(),
90
+ workflowId: options.workflowId || hotmesh_1.HotMeshService.guid(),
92
91
  workflowTopic: workflowTopic,
93
92
  backoffCoefficient: options.config?.backoffCoefficient || factory_1.DEFAULT_COEFFICIENT,
94
93
  };
95
94
  const context = { metadata: { trc, spn }, data: {} };
96
95
  const jobId = await hotMeshClient.pub(`${options.namespace ?? factory_1.APP_ID}.execute`, payload, context);
96
+ //seed search data if present
97
97
  if (jobId && options.search?.data) {
98
- //job successfully kicked off; there is default job data to persist
99
98
  const searchSessionId = `-search-0`;
100
99
  const search = new search_1.Search(jobId, hotMeshClient, searchSessionId);
101
100
  for (const [key, value] of Object.entries(options.search.data)) {
@@ -125,8 +124,17 @@ class ClientService {
125
124
  workflowTopic,
126
125
  backoffCoefficient: options.config?.backoffCoefficient || factory_1.DEFAULT_COEFFICIENT,
127
126
  };
127
+ //seed search data if presentthe hook before entering
128
128
  const hotMeshClient = await this.getHotMeshClient(workflowTopic, options.namespace);
129
- return await hotMeshClient.hook(`${hotMeshClient.appId}.flow.signal`, payload, types_1.StreamStatus.PENDING, 202);
129
+ const msgId = await hotMeshClient.hook(`${hotMeshClient.appId}.flow.signal`, payload, types_1.StreamStatus.PENDING, 202);
130
+ if (options.search?.data) {
131
+ const searchSessionId = `-search-${hotmesh_1.HotMeshService.guid()}-0`;
132
+ const search = new search_1.Search(options.workflowId, hotMeshClient, searchSessionId);
133
+ for (const [key, value] of Object.entries(options.search.data)) {
134
+ search.set(key, value);
135
+ }
136
+ }
137
+ return msgId;
130
138
  },
131
139
  getHandle: async (taskQueue, workflowName, workflowId, namespace) => {
132
140
  const workflowTopic = `${taskQueue}-${workflowName}`;
@@ -1,12 +1,14 @@
1
1
  import { ClientService } from './client';
2
2
  import { ConnectionService } from './connection';
3
3
  import { MeshOSService } from './meshos';
4
+ import { Search } from './search';
4
5
  import { WorkerService } from './worker';
5
6
  import { WorkflowService } from './workflow';
6
7
  import { ContextType } from '../../types/durable';
7
8
  export declare const Durable: {
8
9
  Client: typeof ClientService;
9
10
  Connection: typeof ConnectionService;
11
+ Search: typeof Search;
10
12
  MeshOS: typeof MeshOSService;
11
13
  Worker: typeof WorkerService;
12
14
  workflow: typeof WorkflowService;
@@ -4,11 +4,13 @@ exports.Durable = void 0;
4
4
  const client_1 = require("./client");
5
5
  const connection_1 = require("./connection");
6
6
  const meshos_1 = require("./meshos");
7
+ const search_1 = require("./search");
7
8
  const worker_1 = require("./worker");
8
9
  const workflow_1 = require("./workflow");
9
10
  exports.Durable = {
10
11
  Client: client_1.ClientService,
11
12
  Connection: connection_1.ConnectionService,
13
+ Search: search_1.Search,
12
14
  MeshOS: meshos_1.MeshOSService,
13
15
  Worker: worker_1.WorkerService,
14
16
  workflow: workflow_1.WorkflowService,
@@ -1,52 +1,97 @@
1
1
  import { Search } from './search';
2
2
  import { HotMeshService as HotMesh } from '../hotmesh';
3
- import { ActivityConfig, HookOptions, ProxyType, WorkflowOptions } from "../../types/durable";
3
+ import { ActivityConfig, HookOptions, ProxyType, WorkflowContext, WorkflowOptions } from "../../types/durable";
4
4
  export declare class WorkflowService {
5
5
  /**
6
- * Spawn a child workflow. await and return the result.
6
+ * Spawns a child workflow. await and return the result.
7
+ * @template T - the result type
8
+ * @param {WorkflowOptions} options - the workflow options
9
+ * @returns {Promise<T>} - the result of the child workflow
7
10
  */
8
11
  static executeChild<T>(options: WorkflowOptions): Promise<T>;
9
12
  /**
10
- * spawn a child workflow. return the childJobId.
13
+ * Spawns a child workflow. return the childJobId.
14
+ * This method is used when the result of the child workflow is not needed.
15
+ * @param {WorkflowOptions} options - the workflow options
16
+ * @returns {Promise<string>} - the childJobId
11
17
  */
12
18
  static startChild<T>(options: WorkflowOptions): Promise<string>;
13
19
  /**
14
- * wrap all activities in a proxy that will durably run them
20
+ * Wraps activities in a proxy that will durably run them
21
+ * @param {ActivityConfig} options - the activity configuration
22
+ * that will be used to wrap the activities. You must pass an
23
+ * `activities` object to this configuration. The activities object
24
+ * should be a key-value pair of activity names and their respective
25
+ * functions. This is typically done by importing the activities.
26
+ *
27
+ * @returns {ProxyType<ACT>} - a proxy object with the same keys as the
28
+ * activities object, but with the values replaced by a wrapped function
29
+ * @example
30
+ * // import the activities
31
+ * import * as activities from './activities';
32
+ * const proxy = WorkflowService.proxyActivities<typeof activities>({ activities });
33
+ *
34
+ * //or destructure the proxy object, as the function names are the keys
35
+ * const { activity1, activity2 } = WorkflowService.proxyActivities<typeof activities>({ activities });
15
36
  */
16
37
  static proxyActivities<ACT>(options?: ActivityConfig): ProxyType<ACT>;
17
38
  /**
18
- * return a search session for use when reading/writing to the workflow HASH
39
+ * Returns a search session for use when reading/writing to the workflow HASH.
40
+ * The search session provides access to methods like `get`, `mget`, `set`, `del`, and `incr`.
41
+ * @returns {Promise<Search>} - a search session
19
42
  */
20
43
  static search(): Promise<Search>;
21
44
  /**
22
- * return a handle to the hotmesh client currently running the workflow
45
+ * Return a handle to the hotmesh client currently running the workflow
46
+ * @returns {Promise<HotMesh>} - a hotmesh client
23
47
  */
24
48
  static getHotMesh(): Promise<HotMesh>;
25
49
  /**
26
- * those methods that may only be called once must be protected by flagging
50
+ * Returns the current workflow context
51
+ * @returns {WorkflowContext} - the current workflow context
52
+ */
53
+ static getContext(): WorkflowContext;
54
+ /**
55
+ * Those methods that may only be called once must be protected by flagging
27
56
  * their execution with a unique key (the key is stored in the HASH alongside
28
57
  * process state and job state)
58
+ * @private
29
59
  */
30
60
  static isSideEffectAllowed(hotMeshClient: HotMesh, prefix: string): Promise<boolean>;
31
61
  /**
32
- * returns a random number between 0 and 1. This number is deterministic
62
+ * Returns a random number between 0 and 1. This number is deterministic
33
63
  * and will never vary for a given seed. This is useful for randomizing
34
64
  * pathways in a workflow that can be safely replayed.
35
- * @returns {number}
65
+ * @returns {number} - a random number between 0 and 1
36
66
  */
37
67
  static random(): number;
38
68
  /**
39
- * send signal data into any other paused thread (which is paused and
69
+ * Sends signal data into any other paused thread (which is paused and
40
70
  * awaiting the signal) from within a hook-thread or the main-thread
71
+ * @param {string} signalId - the signal id
72
+ * @param {Record<any, any>} data - the signal data
73
+ * @returns {Promise<string>} - the stream id
41
74
  */
42
75
  static signal(signalId: string, data: Record<any, any>): Promise<string>;
43
76
  /**
44
- * spawn a hook from either the main thread or a hook thread with
77
+ * Spawns a hook from either the main thread or a hook thread with
45
78
  * the provided options; worflowId/TaskQueue/Name are optional and will
46
79
  * default to the current workflowId/WorkflowTopic if not provided
80
+ * @param {HookOptions} options - the hook options
47
81
  */
48
82
  static hook(options: HookOptions): Promise<string>;
83
+ /**
84
+ * Sleeps for a duration.
85
+ * @param {string} duration - for example: '1 minute', '2 hours', '3 days'
86
+ * @returns {Promise<number>}
87
+ */
49
88
  static sleep(duration: string): Promise<number>;
89
+ /**
90
+ * Waits for a signal to awaken
91
+ * @param {string[]} signals - the signals to wait for
92
+ * @param {Record<string, string>} options - the options
93
+ * @returns {Promise<Record<any, any>[]>}
94
+ */
50
95
  static waitForSignal(signals: string[], options?: Record<string, string>): Promise<Record<any, any>[]>;
51
96
  static wrapActivity<T>(activityName: string, options?: ActivityConfig): T;
52
97
  }
@@ -17,7 +17,10 @@ const stream_1 = require("../../types/stream");
17
17
  const utils_1 = require("../../modules/utils");
18
18
  class WorkflowService {
19
19
  /**
20
- * Spawn a child workflow. await and return the result.
20
+ * Spawns a child workflow. await and return the result.
21
+ * @template T - the result type
22
+ * @param {WorkflowOptions} options - the workflow options
23
+ * @returns {Promise<T>} - the result of the child workflow
21
24
  */
22
25
  static async executeChild(options) {
23
26
  const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
@@ -28,15 +31,16 @@ class WorkflowService {
28
31
  const workflowSpan = store.get('workflowSpan');
29
32
  const COUNTER = store.get('counter');
30
33
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
31
- //this is risky but MUST be allowed. Users MAY set the workflowId,
32
- //but if there is a naming collision, the data from the target entity will be used
33
- //as there is know way of knowing if the item was generated via a prior run of the workflow
34
- const childJobId = options.workflowId ?? `-${workflowId}-$${options.workflowName}${workflowDimension}-${execIndex}`;
34
+ //NOTE: this is the hash prefix; necessary for the search index to locate the entity
35
+ //if the hash is a helper, a dash begins it, so it isn't indexed
36
+ const entityOrEmptyString = options.entity ?? '';
37
+ //If the workflowId is not provided, it is generated from the entity and the workflow name
38
+ const childJobId = options.workflowId ?? `${entityOrEmptyString}-${workflowId}-$${options.entity ?? options.workflowName}${workflowDimension}-${execIndex}`;
35
39
  const parentWorkflowId = `${workflowId}-f`;
36
40
  const client = new client_1.ClientService({
37
41
  connection: await connection_1.ConnectionService.connect(worker_1.WorkerService.connection),
38
42
  });
39
- let handle = await client.workflow.getHandle(options.taskQueue, options.workflowName, childJobId, namespace);
43
+ let handle = await client.workflow.getHandle(options.entity ?? options.taskQueue, options.entity ?? options.workflowName, childJobId, namespace);
40
44
  try {
41
45
  return await handle.result(true);
42
46
  }
@@ -55,7 +59,10 @@ class WorkflowService {
55
59
  }
56
60
  }
57
61
  /**
58
- * spawn a child workflow. return the childJobId.
62
+ * Spawns a child workflow. return the childJobId.
63
+ * This method is used when the result of the child workflow is not needed.
64
+ * @param {WorkflowOptions} options - the workflow options
65
+ * @returns {Promise<string>} - the childJobId
59
66
  */
60
67
  static async startChild(options) {
61
68
  const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
@@ -66,9 +73,12 @@ class WorkflowService {
66
73
  const workflowSpan = store.get('workflowSpan');
67
74
  const COUNTER = store.get('counter');
68
75
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
69
- const childJobId = options.workflowId ?? `-${workflowId}-$${options.workflowName}${workflowDimension}-${execIndex}`;
76
+ //NOTE: this is the hash prefix; necessary for the search index to locate the entity
77
+ const entityOrEmptyString = options.entity ?? '';
78
+ //If the workflowId is not provided, it is generated from the entity and the workflow name
79
+ const childJobId = options.workflowId ?? `${entityOrEmptyString}-${workflowId}-$${options.entity ?? options.workflowName}${workflowDimension}-${execIndex}`;
70
80
  const parentWorkflowId = `${workflowId}-f`;
71
- const workflowTopic = `${options.taskQueue}-${options.workflowName}`;
81
+ const workflowTopic = `${options.entity ?? options.taskQueue}-${options.entity ?? options.workflowName}`;
72
82
  try {
73
83
  //get the status; if there is no error, return childJobId (what was spawned)
74
84
  const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
@@ -91,7 +101,22 @@ class WorkflowService {
91
101
  }
92
102
  }
93
103
  /**
94
- * wrap all activities in a proxy that will durably run them
104
+ * Wraps activities in a proxy that will durably run them
105
+ * @param {ActivityConfig} options - the activity configuration
106
+ * that will be used to wrap the activities. You must pass an
107
+ * `activities` object to this configuration. The activities object
108
+ * should be a key-value pair of activity names and their respective
109
+ * functions. This is typically done by importing the activities.
110
+ *
111
+ * @returns {ProxyType<ACT>} - a proxy object with the same keys as the
112
+ * activities object, but with the values replaced by a wrapped function
113
+ * @example
114
+ * // import the activities
115
+ * import * as activities from './activities';
116
+ * const proxy = WorkflowService.proxyActivities<typeof activities>({ activities });
117
+ *
118
+ * //or destructure the proxy object, as the function names are the keys
119
+ * const { activity1, activity2 } = WorkflowService.proxyActivities<typeof activities>({ activities });
95
120
  */
96
121
  static proxyActivities(options) {
97
122
  if (options.activities) {
@@ -108,7 +133,9 @@ class WorkflowService {
108
133
  return proxy;
109
134
  }
110
135
  /**
111
- * return a search session for use when reading/writing to the workflow HASH
136
+ * Returns a search session for use when reading/writing to the workflow HASH.
137
+ * The search session provides access to methods like `get`, `mget`, `set`, `del`, and `incr`.
138
+ * @returns {Promise<Search>} - a search session
112
139
  */
113
140
  static async search() {
114
141
  const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
@@ -124,7 +151,8 @@ class WorkflowService {
124
151
  return new search_1.Search(workflowId, hotMeshClient, searchSessionId);
125
152
  }
126
153
  /**
127
- * return a handle to the hotmesh client currently running the workflow
154
+ * Return a handle to the hotmesh client currently running the workflow
155
+ * @returns {Promise<HotMesh>} - a hotmesh client
128
156
  */
129
157
  static async getHotMesh() {
130
158
  const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
@@ -133,9 +161,33 @@ class WorkflowService {
133
161
  return await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
134
162
  }
135
163
  /**
136
- * those methods that may only be called once must be protected by flagging
164
+ * Returns the current workflow context
165
+ * @returns {WorkflowContext} - the current workflow context
166
+ */
167
+ static getContext() {
168
+ const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
169
+ const workflowId = store.get('workflowId');
170
+ const workflowDimension = store.get('workflowDimension') ?? '';
171
+ const workflowTopic = store.get('workflowTopic');
172
+ const namespace = store.get('namespace');
173
+ const workflowTrace = store.get('workflowTrace');
174
+ const workflowSpan = store.get('workflowSpan');
175
+ const COUNTER = store.get('counter');
176
+ return {
177
+ counter: COUNTER.counter,
178
+ namespace,
179
+ workflowId,
180
+ workflowDimension,
181
+ workflowTopic,
182
+ workflowTrace,
183
+ workflowSpan,
184
+ };
185
+ }
186
+ /**
187
+ * Those methods that may only be called once must be protected by flagging
137
188
  * their execution with a unique key (the key is stored in the HASH alongside
138
189
  * process state and job state)
190
+ * @private
139
191
  */
140
192
  static async isSideEffectAllowed(hotMeshClient, prefix) {
141
193
  const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
@@ -153,10 +205,10 @@ class WorkflowService {
153
205
  return guidValue === 1;
154
206
  }
155
207
  /**
156
- * returns a random number between 0 and 1. This number is deterministic
208
+ * Returns a random number between 0 and 1. This number is deterministic
157
209
  * and will never vary for a given seed. This is useful for randomizing
158
210
  * pathways in a workflow that can be safely replayed.
159
- * @returns {number}
211
+ * @returns {number} - a random number between 0 and 1
160
212
  */
161
213
  static random() {
162
214
  const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
@@ -165,8 +217,11 @@ class WorkflowService {
165
217
  return (0, utils_1.deterministicRandom)(seed);
166
218
  }
167
219
  /**
168
- * send signal data into any other paused thread (which is paused and
220
+ * Sends signal data into any other paused thread (which is paused and
169
221
  * awaiting the signal) from within a hook-thread or the main-thread
222
+ * @param {string} signalId - the signal id
223
+ * @param {Record<any, any>} data - the signal data
224
+ * @returns {Promise<string>} - the stream id
170
225
  */
171
226
  static async signal(signalId, data) {
172
227
  const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
@@ -180,9 +235,10 @@ class WorkflowService {
180
235
  }
181
236
  }
182
237
  /**
183
- * spawn a hook from either the main thread or a hook thread with
238
+ * Spawns a hook from either the main thread or a hook thread with
184
239
  * the provided options; worflowId/TaskQueue/Name are optional and will
185
240
  * default to the current workflowId/WorkflowTopic if not provided
241
+ * @param {HookOptions} options - the hook options
186
242
  */
187
243
  static async hook(options) {
188
244
  const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
@@ -193,8 +249,8 @@ class WorkflowService {
193
249
  const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
194
250
  const workflowId = options.workflowId ?? store.get('workflowId');
195
251
  let workflowTopic = store.get('workflowTopic');
196
- if (options.taskQueue && options.workflowName) {
197
- workflowTopic = `${options.taskQueue}-${options.workflowName}`;
252
+ if (options.entity || (options.taskQueue && options.workflowName)) {
253
+ workflowTopic = `${options.entity ?? options.taskQueue}-${options.entity ?? options.workflowName}`;
198
254
  } //else this is essentially recursion as the function calls itself
199
255
  const payload = {
200
256
  arguments: [...options.args],
@@ -205,6 +261,11 @@ class WorkflowService {
205
261
  return await hotMeshClient.hook(`${namespace}.flow.signal`, payload, stream_1.StreamStatus.PENDING, 202);
206
262
  }
207
263
  }
264
+ /**
265
+ * Sleeps for a duration.
266
+ * @param {string} duration - for example: '1 minute', '2 hours', '3 days'
267
+ * @returns {Promise<number>}
268
+ */
208
269
  static async sleep(duration) {
209
270
  const seconds = (0, ms_1.default)(duration) / 1000;
210
271
  const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
@@ -227,6 +288,12 @@ class WorkflowService {
227
288
  throw new errors_1.DurableSleepError(workflowId, seconds, execIndex, workflowDimension);
228
289
  }
229
290
  }
291
+ /**
292
+ * Waits for a signal to awaken
293
+ * @param {string[]} signals - the signals to wait for
294
+ * @param {Record<string, string>} options - the options
295
+ * @returns {Promise<Record<any, any>[]>}
296
+ */
230
297
  static async waitForSignal(signals, options) {
231
298
  const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
232
299
  const COUNTER = store.get('counter');
@@ -220,7 +220,6 @@ class StreamSignaler {
220
220
  for (const instance of [...StreamSignaler.signalers]) {
221
221
  instance.stopConsuming();
222
222
  }
223
- await (0, utils_1.sleepFor)(BLOCK_TIME_MS);
224
223
  }
225
224
  async stopConsuming() {
226
225
  this.shouldConsume = false;
@@ -5,6 +5,36 @@ type WorkflowConfig = {
5
5
  maximumInterval?: string;
6
6
  initialInterval?: string;
7
7
  };
8
+ type WorkflowContext = {
9
+ /**
10
+ * the reentrant semaphore, incremented in real-time as idempotent statements are re-traversed upon reentry. Indicates the current semaphore count.
11
+ */
12
+ counter: number;
13
+ /**
14
+ * the HotMesh App namespace. `durable` is the default.
15
+ */
16
+ namespace: string;
17
+ /**
18
+ * the workflow/job ID
19
+ */
20
+ workflowId: string;
21
+ /**
22
+ * the dimensional isolation for the reentrant hook, expressed in the format `0,0`, `0,1`, etc
23
+ */
24
+ workflowDimension: string;
25
+ /**
26
+ * a concatenation of the task queue and workflow name (e.g., `${taskQueueName}-${workflowName}`)
27
+ */
28
+ workflowTopic: string;
29
+ /**
30
+ * the open telemetry trace context for the workflow, used for logging and tracing. If a sink is enabled, this will be sent to the sink.
31
+ */
32
+ workflowTrace: string;
33
+ /**
34
+ * the open telemetry span context for the workflow, used for logging and tracing. If a sink is enabled, this will be sent to the sink.
35
+ */
36
+ workflowSpan: string;
37
+ };
8
38
  type WorkflowSearchOptions = {
9
39
  index?: string;
10
40
  prefix?: string[];
@@ -19,6 +49,7 @@ type WorkflowOptions = {
19
49
  taskQueue: string;
20
50
  args: any[];
21
51
  workflowId?: string;
52
+ entity?: string;
22
53
  workflowName?: string;
23
54
  parentWorkflowId?: string;
24
55
  workflowTrace?: string;
@@ -30,6 +61,7 @@ type HookOptions = {
30
61
  namespace?: string;
31
62
  taskQueue?: string;
32
63
  args: any[];
64
+ entity?: string;
33
65
  workflowId?: string;
34
66
  workflowName?: string;
35
67
  search?: WorkflowSearchOptions;
@@ -142,4 +174,4 @@ type ActivityConfig = {
142
174
  maximumInterval: string;
143
175
  };
144
176
  };
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, };
177
+ 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, WorkflowContext, };
@@ -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, FindWhereOptions, FindWhereQuery, HookOptions, MeshOSActivityOptions, MeshOSWorkerOptions, 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, WorkflowContext, 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.29",
3
+ "version": "0.0.31",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -54,7 +54,7 @@
54
54
  "test:durable:loopactivity": "NODE_ENV=test jest ./tests/durable/loopactivity/index.test.ts --detectOpenHandles --forceExit --verbose",
55
55
  "test:durable:nested": "NODE_ENV=test jest ./tests/durable/nested/index.test.ts --detectOpenHandles --forceExit --verbose"
56
56
  },
57
- "keywords": [],
57
+ "keywords": ["durable workflow", "hotmesh", "service mesh", "workflows", "operational data", "redis"],
58
58
  "author": "luke.birdeau@gmail.com",
59
59
  "license": "SEE LICENSE IN LICENSE",
60
60
  "dependencies": {
@@ -1,4 +1,3 @@
1
- import { nanoid } from 'nanoid';
2
1
  import { APP_ID, APP_VERSION, DEFAULT_COEFFICIENT, getWorkflowYAML } from './factory';
3
2
  import { WorkflowHandleService } from './handle';
4
3
  import { HotMeshService as HotMesh } from '../hotmesh';
@@ -104,7 +103,7 @@ export class ClientService {
104
103
  const payload = {
105
104
  arguments: [...options.args],
106
105
  parentWorkflowId: options.parentWorkflowId,
107
- workflowId: options.workflowId || nanoid(),
106
+ workflowId: options.workflowId || HotMesh.guid(),
108
107
  workflowTopic: workflowTopic,
109
108
  backoffCoefficient: options.config?.backoffCoefficient || DEFAULT_COEFFICIENT,
110
109
  }
@@ -112,9 +111,10 @@ export class ClientService {
112
111
  const jobId = await hotMeshClient.pub(
113
112
  `${options.namespace ?? APP_ID}.execute`,
114
113
  payload,
115
- context as JobState);
116
- if (jobId && options.search?.data) {
117
- //job successfully kicked off; there is default job data to persist
114
+ context as JobState
115
+ );
116
+ //seed search data if present
117
+ if (jobId && options.search?.data) {
118
118
  const searchSessionId = `-search-0`;
119
119
  const search = new Search(jobId, hotMeshClient, searchSessionId);
120
120
  for (const [key, value] of Object.entries(options.search.data)) {
@@ -146,8 +146,17 @@ export class ClientService {
146
146
  workflowTopic,
147
147
  backoffCoefficient: options.config?.backoffCoefficient || DEFAULT_COEFFICIENT,
148
148
  }
149
+ //seed search data if presentthe hook before entering
149
150
  const hotMeshClient = await this.getHotMeshClient(workflowTopic, options.namespace);
150
- return await hotMeshClient.hook(`${hotMeshClient.appId}.flow.signal`, payload, StreamStatus.PENDING, 202);
151
+ const msgId = await hotMeshClient.hook(`${hotMeshClient.appId}.flow.signal`, payload, StreamStatus.PENDING, 202);
152
+ if (options.search?.data) {
153
+ const searchSessionId = `-search-${HotMesh.guid()}-0`;
154
+ const search = new Search(options.workflowId, hotMeshClient, searchSessionId);
155
+ for (const [key, value] of Object.entries(options.search.data)) {
156
+ search.set(key, value);
157
+ }
158
+ }
159
+ return msgId;
151
160
  },
152
161
 
153
162
  getHandle: async (taskQueue: string, workflowName: string, workflowId: string, namespace?: string): Promise<WorkflowHandleService> => {
@@ -1,6 +1,7 @@
1
1
  import { ClientService } from './client';
2
2
  import { ConnectionService } from './connection';
3
3
  import { MeshOSService } from './meshos';
4
+ import { Search } from './search';
4
5
  import { WorkerService } from './worker';
5
6
  import { WorkflowService } from './workflow';
6
7
  import { ContextType } from '../../types/durable';
@@ -8,6 +9,7 @@ import { ContextType } from '../../types/durable';
8
9
  export const Durable = {
9
10
  Client: ClientService,
10
11
  Connection: ConnectionService,
12
+ Search,
11
13
  MeshOS: MeshOSService,
12
14
  Worker: WorkerService,
13
15
  workflow: WorkflowService,
@@ -16,6 +16,7 @@ import {
16
16
  ActivityConfig,
17
17
  HookOptions,
18
18
  ProxyType,
19
+ WorkflowContext,
19
20
  WorkflowOptions } from "../../types/durable";
20
21
  import { JobOutput, JobState } from '../../types/job';
21
22
  import { StreamStatus } from '../../types/stream';
@@ -24,7 +25,10 @@ import { deterministicRandom } from '../../modules/utils';
24
25
  export class WorkflowService {
25
26
 
26
27
  /**
27
- * Spawn a child workflow. await and return the result.
28
+ * Spawns a child workflow. await and return the result.
29
+ * @template T - the result type
30
+ * @param {WorkflowOptions} options - the workflow options
31
+ * @returns {Promise<T>} - the result of the child workflow
28
32
  */
29
33
  static async executeChild<T>(options: WorkflowOptions): Promise<T> {
30
34
  const store = asyncLocalStorage.getStore();
@@ -35,10 +39,11 @@ export class WorkflowService {
35
39
  const workflowSpan = store.get('workflowSpan');
36
40
  const COUNTER = store.get('counter');
37
41
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
38
- //this is risky but MUST be allowed. Users MAY set the workflowId,
39
- //but if there is a naming collision, the data from the target entity will be used
40
- //as there is know way of knowing if the item was generated via a prior run of the workflow
41
- const childJobId = options.workflowId ?? `-${workflowId}-$${options.workflowName}${workflowDimension}-${execIndex}`;
42
+ //NOTE: this is the hash prefix; necessary for the search index to locate the entity
43
+ //if the hash is a helper, a dash begins it, so it isn't indexed
44
+ const entityOrEmptyString = options.entity ?? '';
45
+ //If the workflowId is not provided, it is generated from the entity and the workflow name
46
+ const childJobId = options.workflowId ?? `${entityOrEmptyString}-${workflowId}-$${options.entity ?? options.workflowName}${workflowDimension}-${execIndex}`;
42
47
  const parentWorkflowId = `${workflowId}-f`;
43
48
 
44
49
  const client = new Client({
@@ -46,8 +51,8 @@ export class WorkflowService {
46
51
  });
47
52
 
48
53
  let handle = await client.workflow.getHandle(
49
- options.taskQueue,
50
- options.workflowName,
54
+ options.entity ?? options.taskQueue,
55
+ options.entity ?? options.workflowName,
51
56
  childJobId,
52
57
  namespace,
53
58
  );
@@ -70,7 +75,10 @@ export class WorkflowService {
70
75
  }
71
76
 
72
77
  /**
73
- * spawn a child workflow. return the childJobId.
78
+ * Spawns a child workflow. return the childJobId.
79
+ * This method is used when the result of the child workflow is not needed.
80
+ * @param {WorkflowOptions} options - the workflow options
81
+ * @returns {Promise<string>} - the childJobId
74
82
  */
75
83
  static async startChild<T>(options: WorkflowOptions): Promise<string> {
76
84
  const store = asyncLocalStorage.getStore();
@@ -81,9 +89,12 @@ export class WorkflowService {
81
89
  const workflowSpan = store.get('workflowSpan');
82
90
  const COUNTER = store.get('counter');
83
91
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
84
- const childJobId = options.workflowId ?? `-${workflowId}-$${options.workflowName}${workflowDimension}-${execIndex}`;
92
+ //NOTE: this is the hash prefix; necessary for the search index to locate the entity
93
+ const entityOrEmptyString = options.entity ?? '';
94
+ //If the workflowId is not provided, it is generated from the entity and the workflow name
95
+ const childJobId = options.workflowId ?? `${entityOrEmptyString}-${workflowId}-$${options.entity ?? options.workflowName}${workflowDimension}-${execIndex}`;
85
96
  const parentWorkflowId = `${workflowId}-f`;
86
- const workflowTopic = `${options.taskQueue}-${options.workflowName}`;
97
+ const workflowTopic = `${options.entity ?? options.taskQueue}-${options.entity ?? options.workflowName}`;
87
98
 
88
99
  try {
89
100
  //get the status; if there is no error, return childJobId (what was spawned)
@@ -108,7 +119,22 @@ export class WorkflowService {
108
119
  }
109
120
 
110
121
  /**
111
- * wrap all activities in a proxy that will durably run them
122
+ * Wraps activities in a proxy that will durably run them
123
+ * @param {ActivityConfig} options - the activity configuration
124
+ * that will be used to wrap the activities. You must pass an
125
+ * `activities` object to this configuration. The activities object
126
+ * should be a key-value pair of activity names and their respective
127
+ * functions. This is typically done by importing the activities.
128
+ *
129
+ * @returns {ProxyType<ACT>} - a proxy object with the same keys as the
130
+ * activities object, but with the values replaced by a wrapped function
131
+ * @example
132
+ * // import the activities
133
+ * import * as activities from './activities';
134
+ * const proxy = WorkflowService.proxyActivities<typeof activities>({ activities });
135
+ *
136
+ * //or destructure the proxy object, as the function names are the keys
137
+ * const { activity1, activity2 } = WorkflowService.proxyActivities<typeof activities>({ activities });
112
138
  */
113
139
  static proxyActivities<ACT>(options?: ActivityConfig): ProxyType<ACT> {
114
140
  if (options.activities) {
@@ -127,7 +153,9 @@ export class WorkflowService {
127
153
  }
128
154
 
129
155
  /**
130
- * return a search session for use when reading/writing to the workflow HASH
156
+ * Returns a search session for use when reading/writing to the workflow HASH.
157
+ * The search session provides access to methods like `get`, `mget`, `set`, `del`, and `incr`.
158
+ * @returns {Promise<Search>} - a search session
131
159
  */
132
160
  static async search(): Promise<Search> {
133
161
  const store = asyncLocalStorage.getStore();
@@ -144,7 +172,8 @@ export class WorkflowService {
144
172
  }
145
173
 
146
174
  /**
147
- * return a handle to the hotmesh client currently running the workflow
175
+ * Return a handle to the hotmesh client currently running the workflow
176
+ * @returns {Promise<HotMesh>} - a hotmesh client
148
177
  */
149
178
  static async getHotMesh(): Promise<HotMesh> {
150
179
  const store = asyncLocalStorage.getStore();
@@ -154,9 +183,34 @@ export class WorkflowService {
154
183
  }
155
184
 
156
185
  /**
157
- * those methods that may only be called once must be protected by flagging
186
+ * Returns the current workflow context
187
+ * @returns {WorkflowContext} - the current workflow context
188
+ */
189
+ static getContext(): WorkflowContext {
190
+ const store = asyncLocalStorage.getStore();
191
+ const workflowId = store.get('workflowId');
192
+ const workflowDimension = store.get('workflowDimension') ?? '';
193
+ const workflowTopic = store.get('workflowTopic');
194
+ const namespace = store.get('namespace');
195
+ const workflowTrace = store.get('workflowTrace');
196
+ const workflowSpan = store.get('workflowSpan');
197
+ const COUNTER = store.get('counter');
198
+ return {
199
+ counter: COUNTER.counter,
200
+ namespace,
201
+ workflowId,
202
+ workflowDimension,
203
+ workflowTopic,
204
+ workflowTrace,
205
+ workflowSpan,
206
+ };
207
+ }
208
+
209
+ /**
210
+ * Those methods that may only be called once must be protected by flagging
158
211
  * their execution with a unique key (the key is stored in the HASH alongside
159
212
  * process state and job state)
213
+ * @private
160
214
  */
161
215
  static async isSideEffectAllowed(hotMeshClient: HotMesh, prefix:string): Promise<boolean> {
162
216
  const store = asyncLocalStorage.getStore();
@@ -175,10 +229,10 @@ export class WorkflowService {
175
229
  }
176
230
 
177
231
  /**
178
- * returns a random number between 0 and 1. This number is deterministic
232
+ * Returns a random number between 0 and 1. This number is deterministic
179
233
  * and will never vary for a given seed. This is useful for randomizing
180
234
  * pathways in a workflow that can be safely replayed.
181
- * @returns {number}
235
+ * @returns {number} - a random number between 0 and 1
182
236
  */
183
237
  static random(): number {
184
238
  const store = asyncLocalStorage.getStore();
@@ -188,8 +242,11 @@ export class WorkflowService {
188
242
  }
189
243
 
190
244
  /**
191
- * send signal data into any other paused thread (which is paused and
245
+ * Sends signal data into any other paused thread (which is paused and
192
246
  * awaiting the signal) from within a hook-thread or the main-thread
247
+ * @param {string} signalId - the signal id
248
+ * @param {Record<any, any>} data - the signal data
249
+ * @returns {Promise<string>} - the stream id
193
250
  */
194
251
  static async signal(signalId: string, data: Record<any, any>): Promise<string> {
195
252
  const store = asyncLocalStorage.getStore();
@@ -204,9 +261,10 @@ export class WorkflowService {
204
261
  }
205
262
 
206
263
  /**
207
- * spawn a hook from either the main thread or a hook thread with
264
+ * Spawns a hook from either the main thread or a hook thread with
208
265
  * the provided options; worflowId/TaskQueue/Name are optional and will
209
266
  * default to the current workflowId/WorkflowTopic if not provided
267
+ * @param {HookOptions} options - the hook options
210
268
  */
211
269
  static async hook(options: HookOptions): Promise<string> {
212
270
  const store = asyncLocalStorage.getStore();
@@ -217,8 +275,8 @@ export class WorkflowService {
217
275
  const store = asyncLocalStorage.getStore();
218
276
  const workflowId = options.workflowId ?? store.get('workflowId');
219
277
  let workflowTopic = store.get('workflowTopic');
220
- if (options.taskQueue && options.workflowName) {
221
- workflowTopic = `${options.taskQueue}-${options.workflowName}`;
278
+ if (options.entity || (options.taskQueue && options.workflowName)) {
279
+ workflowTopic = `${options.entity ?? options.taskQueue}-${options.entity ?? options.workflowName}`;
222
280
  } //else this is essentially recursion as the function calls itself
223
281
  const payload = {
224
282
  arguments: [...options.args],
@@ -230,6 +288,11 @@ export class WorkflowService {
230
288
  }
231
289
  }
232
290
 
291
+ /**
292
+ * Sleeps for a duration.
293
+ * @param {string} duration - for example: '1 minute', '2 hours', '3 days'
294
+ * @returns {Promise<number>}
295
+ */
233
296
  static async sleep(duration: string): Promise<number> {
234
297
  const seconds = ms(duration) / 1000;
235
298
 
@@ -254,6 +317,12 @@ export class WorkflowService {
254
317
  }
255
318
  }
256
319
 
320
+ /**
321
+ * Waits for a signal to awaken
322
+ * @param {string[]} signals - the signals to wait for
323
+ * @param {Record<string, string>} options - the options
324
+ * @returns {Promise<Record<any, any>[]>}
325
+ */
257
326
  static async waitForSignal(signals: string[], options?: Record<string, string>): Promise<Record<any, any>[]> {
258
327
  const store = asyncLocalStorage.getStore();
259
328
  const COUNTER = store.get('counter');
@@ -254,7 +254,6 @@ class StreamSignaler {
254
254
  for (const instance of [...StreamSignaler.signalers]) {
255
255
  instance.stopConsuming();
256
256
  }
257
- await sleepFor(BLOCK_TIME_MS);
258
257
  }
259
258
 
260
259
  async stopConsuming() {
package/types/durable.ts CHANGED
@@ -7,6 +7,44 @@ type WorkflowConfig = {
7
7
  initialInterval?: string; //default 1s
8
8
  }
9
9
 
10
+ type WorkflowContext = {
11
+
12
+ /**
13
+ * the reentrant semaphore, incremented in real-time as idempotent statements are re-traversed upon reentry. Indicates the current semaphore count.
14
+ */
15
+ counter: number;
16
+
17
+ /**
18
+ * the HotMesh App namespace. `durable` is the default.
19
+ */
20
+ namespace: string;
21
+
22
+ /**
23
+ * the workflow/job ID
24
+ */
25
+ workflowId: string;
26
+
27
+ /**
28
+ * the dimensional isolation for the reentrant hook, expressed in the format `0,0`, `0,1`, etc
29
+ */
30
+ workflowDimension: string;
31
+
32
+ /**
33
+ * a concatenation of the task queue and workflow name (e.g., `${taskQueueName}-${workflowName}`)
34
+ */
35
+ workflowTopic: string;
36
+
37
+ /**
38
+ * the open telemetry trace context for the workflow, used for logging and tracing. If a sink is enabled, this will be sent to the sink.
39
+ */
40
+ workflowTrace: string;
41
+
42
+ /**
43
+ * the open telemetry span context for the workflow, used for logging and tracing. If a sink is enabled, this will be sent to the sink.
44
+ */
45
+ workflowSpan: string;
46
+ }
47
+
10
48
  type WorkflowSearchOptions = {
11
49
  index?: string; //FT index name (myapp:myindex)
12
50
  prefix?: string[]; //FT prefixes (['myapp:myindex:prefix1', 'myapp:myindex:prefix2'])
@@ -15,11 +53,12 @@ type WorkflowSearchOptions = {
15
53
  }
16
54
 
17
55
  type WorkflowOptions = {
18
- namespace?: string; //'durable' is the default namespace if not provided; similar to setting `appid` in the YAML
56
+ namespace?: string; //'durable' is the default namespace if not provided; similar to setting `appid` in the YAML
19
57
  taskQueue: string;
20
- args: any[]; //input arguments to pass in
21
- workflowId?: string; //execution id (the job id)
22
- workflowName?: string; //the name of the user's workflow function
58
+ args: any[]; //input arguments to pass in
59
+ workflowId?: string; //execution id (the job id)
60
+ entity?: string; //If invoking a workflow, passing 'entity' will apply the value as the workflowName, taskQueue, and prefix, ensuring the FT.SEARCH index is properly scoped. This is a convenience method but limits options.
61
+ workflowName?: string; //the name of the user's workflow function
23
62
  parentWorkflowId?: string; //system reserved; the id of the parent; if present the flow will not self-clean until the parent that spawned it self-cleans
24
63
  workflowTrace?: string;
25
64
  workflowSpan?: string;
@@ -31,6 +70,7 @@ type HookOptions = {
31
70
  namespace?: string; //'durable' is the default namespace if not provided; similar to setting `appid` in the YAML
32
71
  taskQueue?: string;
33
72
  args: any[]; //input arguments to pass into the hook
73
+ entity?: string; //If invoking a hook, passing 'entity' will apply the value as the workflowName, taskQueue, and prefix, ensuring the FT.SEARCH index is properly scoped. This is a convenience method but limits options.
34
74
  workflowId?: string; //execution id (the job id to hook into)
35
75
  workflowName?: string; //the name of the user's hook function
36
76
  search?: WorkflowSearchOptions //bind additional search terms immediately before hook reentry
@@ -188,4 +228,5 @@ export {
188
228
  WorkflowSearchOptions,
189
229
  WorkflowDataType,
190
230
  WorkflowOptions,
231
+ WorkflowContext,
191
232
  };
package/types/index.ts CHANGED
@@ -49,6 +49,7 @@ export {
49
49
  WorkflowConfig,
50
50
  WorkerConfig,
51
51
  WorkerOptions,
52
+ WorkflowContext,
52
53
  WorkflowSearchOptions,
53
54
  WorkflowDataType,
54
55
  WorkflowOptions,