@hotmeshio/hotmesh 0.0.20 → 0.0.22

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
@@ -226,9 +226,6 @@ Exchanging data between activities is central to HotMesh. For detailed informati
226
226
  ## Composition
227
227
  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).
228
228
 
229
- ## Architectural First Principles
230
- For a deep dive into HotMesh's distributed orchestration philosophy, refer to the [Architectural First Principles Overview](https://github.com/hotmeshio/sdk-typescript/blob/main/docs/architecture.md).
231
-
232
229
  ## Distributed Orchestration
233
230
  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.
234
231
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -12,10 +12,9 @@ class Search {
12
12
  this.searchSessionIndex = 0;
13
13
  const keyParams = {
14
14
  appId: hotMeshClient.appId,
15
- jobId: ''
15
+ jobId: workflowId
16
16
  };
17
- const hotMeshPrefix = key_1.KeyService.mintKey(hotMeshClient.namespace, key_1.KeyType.JOB_STATE, keyParams);
18
- this.jobId = `${hotMeshPrefix}${workflowId}`;
17
+ this.jobId = key_1.KeyService.mintKey(hotMeshClient.namespace, key_1.KeyType.JOB_STATE, keyParams);
19
18
  this.searchSessionId = searchSessionId;
20
19
  this.hotMeshClient = hotMeshClient;
21
20
  this.store = hotMeshClient.engine.store;
@@ -3,9 +3,13 @@ import { HotMeshService as HotMesh } from '../hotmesh';
3
3
  import { ActivityConfig, HookOptions, ProxyType, WorkflowOptions } from "../../types/durable";
4
4
  export declare class WorkflowService {
5
5
  /**
6
- * Spawn a child workflow. await the result.
6
+ * Spawn a child workflow. await and return the result.
7
7
  */
8
8
  static executeChild<T>(options: WorkflowOptions): Promise<T>;
9
+ /**
10
+ * spawn a child workflow. return the childJobId.
11
+ */
12
+ static startChild<T>(options: WorkflowOptions): Promise<string>;
9
13
  static proxyActivities<ACT>(options?: ActivityConfig): ProxyType<ACT>;
10
14
  static search(): Promise<Search>;
11
15
  /**
@@ -16,37 +16,79 @@ const worker_1 = require("./worker");
16
16
  const stream_1 = require("../../types/stream");
17
17
  class WorkflowService {
18
18
  /**
19
- * Spawn a child workflow. await the result.
19
+ * Spawn a child workflow. await and return the result.
20
20
  */
21
21
  static async executeChild(options) {
22
22
  const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
23
+ const namespace = store.get('namespace');
23
24
  const workflowId = store.get('workflowId');
24
25
  const workflowDimension = store.get('workflowDimension') ?? '';
25
26
  const workflowTrace = store.get('workflowTrace');
26
27
  const workflowSpan = store.get('workflowSpan');
27
28
  const COUNTER = store.get('counter');
28
29
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
29
- const childJobId = `${workflowId}-$${options.workflowName}${workflowDimension}-${execIndex}`;
30
+ //this is risky but MUST be allowed. Users MAY set the workflowId,
31
+ //but if there is a naming collision, the data from the target entity will be used
32
+ //as there is know way of knowing if the item was generated via a prior run of the workflow
33
+ const childJobId = options.workflowId ?? `${workflowId}-$${options.workflowName}${workflowDimension}-${execIndex}`;
30
34
  const parentWorkflowId = `${workflowId}-f`;
31
35
  const client = new client_1.ClientService({
32
36
  connection: await connection_1.ConnectionService.connect(worker_1.WorkerService.connection),
33
37
  });
34
- let handle = await client.workflow.getHandle(options.taskQueue, options.workflowName, childJobId);
38
+ let handle = await client.workflow.getHandle(options.taskQueue, options.workflowName, childJobId, namespace);
35
39
  try {
36
40
  return await handle.result(true);
37
41
  }
38
42
  catch (error) {
39
43
  handle = await client.workflow.start({
40
44
  ...options,
45
+ namespace,
41
46
  workflowId: childJobId,
42
47
  parentWorkflowId,
43
48
  workflowTrace,
44
49
  workflowSpan,
45
50
  });
51
+ //todo: options.startToCloseTimeout
46
52
  const result = await handle.result();
47
53
  return result;
48
54
  }
49
55
  }
56
+ /**
57
+ * spawn a child workflow. return the childJobId.
58
+ */
59
+ static async startChild(options) {
60
+ const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
61
+ const namespace = store.get('namespace');
62
+ const workflowId = store.get('workflowId');
63
+ const workflowDimension = store.get('workflowDimension') ?? '';
64
+ const workflowTrace = store.get('workflowTrace');
65
+ const workflowSpan = store.get('workflowSpan');
66
+ const COUNTER = store.get('counter');
67
+ const execIndex = COUNTER.counter = COUNTER.counter + 1;
68
+ const childJobId = options.workflowId ?? `${workflowId}-$${options.workflowName}${workflowDimension}-${execIndex}`;
69
+ const parentWorkflowId = `${workflowId}-f`;
70
+ const workflowTopic = `${options.taskQueue}-${options.workflowName}`;
71
+ try {
72
+ //get the status; if there is no error, return childJobId (what was spawned)
73
+ const hotMeshClient = await worker_1.WorkerService.getHotMesh(workflowTopic, { namespace });
74
+ await hotMeshClient.getStatus(childJobId);
75
+ return childJobId;
76
+ }
77
+ catch (error) {
78
+ const client = new client_1.ClientService({
79
+ connection: await connection_1.ConnectionService.connect(worker_1.WorkerService.connection),
80
+ });
81
+ await client.workflow.start({
82
+ ...options,
83
+ namespace,
84
+ workflowId: childJobId,
85
+ parentWorkflowId,
86
+ workflowTrace,
87
+ workflowSpan,
88
+ });
89
+ return childJobId;
90
+ }
91
+ }
50
92
  static proxyActivities(options) {
51
93
  if (options.activities) {
52
94
  worker_1.WorkerService.registerActivities(options.activities);
@@ -119,16 +161,11 @@ class WorkflowService {
119
161
  const hotMeshClient = await worker_1.WorkerService.getHotMesh(`${namespace}.flow.signal`, { namespace });
120
162
  if (await WorkflowService.isSideEffectAllowed(hotMeshClient, 'hook')) {
121
163
  const store = asyncLocalStorage_1.asyncLocalStorage.getStore();
122
- let workflowId;
123
- let workflowTopic;
124
- if (options.workflowId && options.taskQueue && options.workflowName) {
125
- workflowId = options.workflowId;
164
+ const workflowId = options.workflowId ?? store.get('workflowId');
165
+ let workflowTopic = store.get('workflowTopic');
166
+ if (options.taskQueue && options.workflowName) {
126
167
  workflowTopic = `${options.taskQueue}-${options.workflowName}`;
127
- }
128
- else {
129
- workflowId = store.get('workflowId');
130
- workflowTopic = store.get('workflowTopic');
131
- }
168
+ } //else this is essentially recursion as the function calls itself
132
169
  const payload = {
133
170
  arguments: [...options.args],
134
171
  id: workflowId,
@@ -18,7 +18,7 @@ type WorkflowOptions = {
18
18
  namespace?: string;
19
19
  taskQueue: string;
20
20
  args: any[];
21
- workflowId: string;
21
+ workflowId?: string;
22
22
  workflowName?: string;
23
23
  parentWorkflowId?: string;
24
24
  workflowTrace?: string;
@@ -28,9 +28,9 @@ type WorkflowOptions = {
28
28
  };
29
29
  type HookOptions = {
30
30
  namespace?: string;
31
- taskQueue: string;
31
+ taskQueue?: string;
32
32
  args: any[];
33
- workflowId: string;
33
+ workflowId?: string;
34
34
  workflowName?: string;
35
35
  search?: WorkflowSearchOptions;
36
36
  config?: WorkflowConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
4
4
  "description": "Unbreakable Workflows",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -19,10 +19,9 @@ export class Search {
19
19
  constructor(workflowId: string, hotMeshClient: HotMesh, searchSessionId: string) {
20
20
  const keyParams = {
21
21
  appId: hotMeshClient.appId,
22
- jobId: ''
22
+ jobId: workflowId
23
23
  }
24
- const hotMeshPrefix = KeyService.mintKey(hotMeshClient.namespace, KeyType.JOB_STATE, keyParams);
25
- this.jobId = `${hotMeshPrefix}${workflowId}`;
24
+ this.jobId = KeyService.mintKey(hotMeshClient.namespace, KeyType.JOB_STATE, keyParams);
26
25
  this.searchSessionId = searchSessionId;
27
26
  this.hotMeshClient = hotMeshClient;
28
27
  this.store = hotMeshClient.engine.store as StoreService<RedisClient, RedisMulti>;
@@ -23,17 +23,21 @@ import { StreamStatus } from '../../types/stream';
23
23
  export class WorkflowService {
24
24
 
25
25
  /**
26
- * Spawn a child workflow. await the result.
26
+ * Spawn a child workflow. await and return the result.
27
27
  */
28
28
  static async executeChild<T>(options: WorkflowOptions): Promise<T> {
29
29
  const store = asyncLocalStorage.getStore();
30
+ const namespace = store.get('namespace');
30
31
  const workflowId = store.get('workflowId');
31
32
  const workflowDimension = store.get('workflowDimension') ?? '';
32
33
  const workflowTrace = store.get('workflowTrace');
33
34
  const workflowSpan = store.get('workflowSpan');
34
35
  const COUNTER = store.get('counter');
35
36
  const execIndex = COUNTER.counter = COUNTER.counter + 1;
36
- const childJobId = `${workflowId}-$${options.workflowName}${workflowDimension}-${execIndex}`;
37
+ //this is risky but MUST be allowed. Users MAY set the workflowId,
38
+ //but if there is a naming collision, the data from the target entity will be used
39
+ //as there is know way of knowing if the item was generated via a prior run of the workflow
40
+ const childJobId = options.workflowId ?? `${workflowId}-$${options.workflowName}${workflowDimension}-${execIndex}`;
37
41
  const parentWorkflowId = `${workflowId}-f`;
38
42
 
39
43
  const client = new Client({
@@ -43,7 +47,8 @@ export class WorkflowService {
43
47
  let handle = await client.workflow.getHandle(
44
48
  options.taskQueue,
45
49
  options.workflowName,
46
- childJobId
50
+ childJobId,
51
+ namespace,
47
52
  );
48
53
 
49
54
  try {
@@ -51,16 +56,56 @@ export class WorkflowService {
51
56
  } catch (error) {
52
57
  handle = await client.workflow.start({
53
58
  ...options,
59
+ namespace,
54
60
  workflowId: childJobId,
55
61
  parentWorkflowId,
56
62
  workflowTrace,
57
63
  workflowSpan,
58
64
  });
65
+ //todo: options.startToCloseTimeout
59
66
  const result = await handle.result();
60
67
  return result as T;
61
68
  }
62
69
  }
63
70
 
71
+ /**
72
+ * spawn a child workflow. return the childJobId.
73
+ */
74
+ static async startChild<T>(options: WorkflowOptions): Promise<string> {
75
+ const store = asyncLocalStorage.getStore();
76
+ const namespace = store.get('namespace');
77
+ const workflowId = store.get('workflowId');
78
+ const workflowDimension = store.get('workflowDimension') ?? '';
79
+ const workflowTrace = store.get('workflowTrace');
80
+ const workflowSpan = store.get('workflowSpan');
81
+ const COUNTER = store.get('counter');
82
+ const execIndex = COUNTER.counter = COUNTER.counter + 1;
83
+ const childJobId = options.workflowId ?? `${workflowId}-$${options.workflowName}${workflowDimension}-${execIndex}`;
84
+ const parentWorkflowId = `${workflowId}-f`;
85
+ const workflowTopic = `${options.taskQueue}-${options.workflowName}`;
86
+
87
+ try {
88
+ //get the status; if there is no error, return childJobId (what was spawned)
89
+ const hotMeshClient = await WorkerService.getHotMesh(workflowTopic, { namespace });
90
+ await hotMeshClient.getStatus(childJobId);
91
+ return childJobId;
92
+ } catch (error) {
93
+ const client = new Client({
94
+ connection: await Connection.connect(WorkerService.connection),
95
+ });
96
+
97
+ await client.workflow.start({
98
+ ...options,
99
+ namespace,
100
+ workflowId: childJobId,
101
+ parentWorkflowId,
102
+ workflowTrace,
103
+ workflowSpan,
104
+ });
105
+ return childJobId;
106
+ }
107
+ }
108
+
64
109
  static proxyActivities<ACT>(options?: ActivityConfig): ProxyType<ACT> {
65
110
  if (options.activities) {
66
111
  WorkerService.registerActivities(options.activities)
@@ -138,15 +183,11 @@ export class WorkflowService {
138
183
  const hotMeshClient = await WorkerService.getHotMesh(`${namespace}.flow.signal`, { namespace });
139
184
  if (await WorkflowService.isSideEffectAllowed(hotMeshClient, 'hook')) {
140
185
  const store = asyncLocalStorage.getStore();
141
- let workflowId: string;
142
- let workflowTopic: string;
143
- if (options.workflowId && options.taskQueue && options.workflowName) {
144
- workflowId = options.workflowId;
186
+ const workflowId = options.workflowId ?? store.get('workflowId');
187
+ let workflowTopic = store.get('workflowTopic');
188
+ if (options.taskQueue && options.workflowName) {
145
189
  workflowTopic = `${options.taskQueue}-${options.workflowName}`;
146
- } else {
147
- workflowId = store.get('workflowId');
148
- workflowTopic = store.get('workflowTopic');
149
- }
190
+ } //else this is essentially recursion as the function calls itself
150
191
  const payload = {
151
192
  arguments: [...options.args],
152
193
  id: workflowId,
package/types/durable.ts CHANGED
@@ -18,7 +18,7 @@ type WorkflowOptions = {
18
18
  namespace?: string; //'durable' is the default namespace if not provided; similar to setting `appid` in the YAML
19
19
  taskQueue: string;
20
20
  args: any[]; //input arguments to pass in
21
- workflowId: string; //execution id (the job id)
21
+ workflowId?: string; //execution id (the job id)
22
22
  workflowName?: string; //the name of the user's workflow function
23
23
  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
24
  workflowTrace?: string;
@@ -29,9 +29,9 @@ type WorkflowOptions = {
29
29
 
30
30
  type HookOptions = {
31
31
  namespace?: string; //'durable' is the default namespace if not provided; similar to setting `appid` in the YAML
32
- taskQueue: string;
32
+ taskQueue?: string;
33
33
  args: any[]; //input arguments to pass into the hook
34
- workflowId: string; //execution id (the job id to hook into)
34
+ workflowId?: string; //execution id (the job id to hook into)
35
35
  workflowName?: string; //the name of the user's hook function
36
36
  search?: WorkflowSearchOptions //bind additional search terms immediately before hook reentry
37
37
  config?: WorkflowConfig; //hook function constraints (backoffCoefficient, maximumAttempts, maximumInterval, initialInterval)