@hotmeshio/hotmesh 0.8.0 → 0.9.0
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/.claude/settings.local.json +2 -1
- package/README.md +158 -38
- package/build/package.json +62 -67
- package/build/services/activities/activity.d.ts +58 -7
- package/build/services/activities/activity.js +66 -37
- package/build/services/activities/await.d.ts +101 -0
- package/build/services/activities/await.js +101 -0
- package/build/services/activities/cycle.d.ts +82 -0
- package/build/services/activities/cycle.js +86 -8
- package/build/services/activities/hook.d.ts +139 -1
- package/build/services/activities/hook.js +140 -2
- package/build/services/activities/interrupt.d.ts +112 -0
- package/build/services/activities/interrupt.js +118 -5
- package/build/services/activities/signal.d.ts +108 -3
- package/build/services/activities/signal.js +113 -8
- package/build/services/activities/trigger.d.ts +56 -4
- package/build/services/activities/trigger.js +119 -35
- package/build/services/activities/worker.d.ts +107 -0
- package/build/services/activities/worker.js +107 -0
- package/build/services/collator/index.d.ts +3 -15
- package/build/services/collator/index.js +7 -34
- package/build/services/engine/index.d.ts +18 -2
- package/build/services/engine/index.js +14 -4
- package/build/services/exporter/index.d.ts +2 -0
- package/build/services/exporter/index.js +1 -0
- package/build/services/hotmesh/index.d.ts +471 -236
- package/build/services/hotmesh/index.js +473 -238
- package/build/services/memflow/client.js +2 -2
- package/build/services/memflow/handle.js +1 -1
- package/build/services/memflow/index.d.ts +1 -1
- package/build/services/memflow/index.js +1 -1
- package/build/services/memflow/workflow/all.d.ts +28 -3
- package/build/services/memflow/workflow/all.js +28 -3
- package/build/services/memflow/workflow/context.d.ts +44 -1
- package/build/services/memflow/workflow/context.js +44 -1
- package/build/services/memflow/workflow/didRun.d.ts +23 -3
- package/build/services/memflow/workflow/didRun.js +23 -3
- package/build/services/memflow/workflow/emit.d.ts +43 -4
- package/build/services/memflow/workflow/emit.js +43 -4
- package/build/services/memflow/workflow/enrich.d.ts +32 -4
- package/build/services/memflow/workflow/enrich.js +32 -4
- package/build/services/memflow/workflow/entityMethods.d.ts +54 -7
- package/build/services/memflow/workflow/entityMethods.js +54 -7
- package/build/services/memflow/workflow/execChild.d.ts +96 -8
- package/build/services/memflow/workflow/execChild.js +96 -8
- package/build/services/memflow/workflow/execHook.d.ts +54 -39
- package/build/services/memflow/workflow/execHook.js +52 -38
- package/build/services/memflow/workflow/execHookBatch.d.ts +82 -29
- package/build/services/memflow/workflow/execHookBatch.js +80 -28
- package/build/services/memflow/workflow/hook.d.ts +68 -3
- package/build/services/memflow/workflow/hook.js +69 -4
- package/build/services/memflow/workflow/index.d.ts +65 -10
- package/build/services/memflow/workflow/index.js +65 -10
- package/build/services/memflow/workflow/interrupt.d.ts +50 -4
- package/build/services/memflow/workflow/interrupt.js +50 -4
- package/build/services/memflow/workflow/interruption.d.ts +49 -16
- package/build/services/memflow/workflow/interruption.js +49 -16
- package/build/services/memflow/workflow/isSideEffectAllowed.d.ts +21 -4
- package/build/services/memflow/workflow/isSideEffectAllowed.js +21 -4
- package/build/services/memflow/workflow/proxyActivities.d.ts +70 -42
- package/build/services/memflow/workflow/proxyActivities.js +70 -42
- package/build/services/memflow/workflow/random.d.ts +33 -3
- package/build/services/memflow/workflow/random.js +33 -3
- package/build/services/memflow/workflow/searchMethods.d.ts +49 -2
- package/build/services/memflow/workflow/searchMethods.js +49 -2
- package/build/services/memflow/workflow/signal.d.ts +51 -22
- package/build/services/memflow/workflow/signal.js +52 -23
- package/build/services/memflow/workflow/sleepFor.d.ts +57 -18
- package/build/services/memflow/workflow/sleepFor.js +57 -18
- package/build/services/memflow/workflow/trace.d.ts +39 -6
- package/build/services/memflow/workflow/trace.js +39 -6
- package/build/services/memflow/workflow/waitFor.d.ts +55 -18
- package/build/services/memflow/workflow/waitFor.js +55 -18
- package/build/services/store/index.d.ts +1 -1
- package/build/services/store/providers/postgres/postgres.d.ts +1 -1
- package/build/services/store/providers/postgres/postgres.js +4 -3
- package/build/services/telemetry/index.js +6 -0
- package/build/types/activity.d.ts +1 -1
- package/build/types/hotmesh.d.ts +1 -1
- package/build/types/job.d.ts +1 -1
- package/build/types/memflow.d.ts +1 -1
- package/build/types/quorum.d.ts +2 -2
- package/build/vitest.config.d.ts +2 -0
- package/build/vitest.config.js +18 -0
- package/package.json +62 -67
- package/vitest.config.ts +17 -0
|
@@ -1,18 +1,106 @@
|
|
|
1
1
|
import { WorkflowOptions } from './common';
|
|
2
2
|
/**
|
|
3
|
-
* Spawns a child workflow and awaits
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* Spawns a child workflow and awaits its result. The child runs as an
|
|
4
|
+
* independent job with its own lifecycle, retry policy, and dimensional
|
|
5
|
+
* isolation. If the child fails, the error is propagated to the parent
|
|
6
|
+
* as a typed error (`MemFlowFatalError`, `MemFlowMaxedError`,
|
|
7
|
+
* `MemFlowTimeoutError`, or `MemFlowRetryError`).
|
|
8
|
+
*
|
|
9
|
+
* On replay, the stored child result is returned immediately without
|
|
10
|
+
* re-spawning the child workflow.
|
|
11
|
+
*
|
|
12
|
+
* ## Child Job ID
|
|
13
|
+
*
|
|
14
|
+
* If `options.workflowId` is provided, it is used directly. Otherwise,
|
|
15
|
+
* the child ID is generated from the entity/workflow name, a GUID, the
|
|
16
|
+
* parent's dimensional coordinates, and the execution index — ensuring
|
|
17
|
+
* uniqueness across parallel and re-entrant executions.
|
|
18
|
+
*
|
|
19
|
+
* ## Examples
|
|
20
|
+
*
|
|
21
|
+
* ```typescript
|
|
22
|
+
* import { MemFlow } from '@hotmeshio/hotmesh';
|
|
23
|
+
*
|
|
24
|
+
* // Spawn a child workflow and await its result
|
|
25
|
+
* export async function parentWorkflow(orderId: string): Promise<string> {
|
|
26
|
+
* const result = await MemFlow.workflow.execChild<{ status: string }>({
|
|
27
|
+
* taskQueue: 'payments',
|
|
28
|
+
* workflowName: 'processPayment',
|
|
29
|
+
* args: [orderId, 99.99],
|
|
30
|
+
* config: {
|
|
31
|
+
* maximumAttempts: 3,
|
|
32
|
+
* backoffCoefficient: 2,
|
|
33
|
+
* },
|
|
34
|
+
* });
|
|
35
|
+
* return result.status;
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* ```typescript
|
|
40
|
+
* // Fan-out: spawn multiple children in parallel
|
|
41
|
+
* export async function batchWorkflow(items: string[]): Promise<string[]> {
|
|
42
|
+
* const results = await Promise.all(
|
|
43
|
+
* items.map((item) =>
|
|
44
|
+
* MemFlow.workflow.execChild<string>({
|
|
45
|
+
* taskQueue: 'processors',
|
|
46
|
+
* workflowName: 'processItem',
|
|
47
|
+
* args: [item],
|
|
48
|
+
* }),
|
|
49
|
+
* ),
|
|
50
|
+
* );
|
|
51
|
+
* return results;
|
|
52
|
+
* }
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* ```typescript
|
|
56
|
+
* // Entity-based child (uses entity name as task queue)
|
|
57
|
+
* const user = await MemFlow.workflow.execChild<UserRecord>({
|
|
58
|
+
* entity: 'user',
|
|
59
|
+
* args: [{ name: 'Alice', email: 'alice@example.com' }],
|
|
60
|
+
* workflowId: 'user-alice', // deterministic ID
|
|
61
|
+
* expire: 3600, // 1 hour TTL
|
|
62
|
+
* });
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* @template T - The return type of the child workflow.
|
|
66
|
+
* @param {WorkflowOptions} options - Child workflow configuration.
|
|
67
|
+
* @returns {Promise<T>} The child workflow's return value.
|
|
7
68
|
*/
|
|
8
69
|
export declare function execChild<T>(options: WorkflowOptions): Promise<T>;
|
|
9
70
|
/**
|
|
10
|
-
* Alias for execChild.
|
|
71
|
+
* Alias for {@link execChild}.
|
|
11
72
|
*/
|
|
12
73
|
export declare const executeChild: typeof execChild;
|
|
13
74
|
/**
|
|
14
|
-
* Spawns a child workflow and
|
|
15
|
-
*
|
|
16
|
-
*
|
|
75
|
+
* Spawns a child workflow in fire-and-forget mode. The parent workflow
|
|
76
|
+
* continues immediately without waiting for the child to complete.
|
|
77
|
+
* Returns the child's job ID for later reference (e.g., to interrupt
|
|
78
|
+
* or query the child).
|
|
79
|
+
*
|
|
80
|
+
* This is a convenience wrapper around `execChild` with `await: false`.
|
|
81
|
+
*
|
|
82
|
+
* ## Example
|
|
83
|
+
*
|
|
84
|
+
* ```typescript
|
|
85
|
+
* import { MemFlow } from '@hotmeshio/hotmesh';
|
|
86
|
+
*
|
|
87
|
+
* export async function dispatchWorkflow(taskId: string): Promise<string> {
|
|
88
|
+
* // Fire-and-forget: start the child and continue immediately
|
|
89
|
+
* const childJobId = await MemFlow.workflow.startChild({
|
|
90
|
+
* taskQueue: 'background',
|
|
91
|
+
* workflowName: 'longRunningTask',
|
|
92
|
+
* args: [taskId],
|
|
93
|
+
* });
|
|
94
|
+
*
|
|
95
|
+
* // Optionally store the child ID for monitoring
|
|
96
|
+
* const search = await MemFlow.workflow.search();
|
|
97
|
+
* await search.set({ childJobId });
|
|
98
|
+
*
|
|
99
|
+
* return childJobId;
|
|
100
|
+
* }
|
|
101
|
+
* ```
|
|
102
|
+
*
|
|
103
|
+
* @param {WorkflowOptions} options - Child workflow configuration.
|
|
104
|
+
* @returns {Promise<string>} The child workflow's job ID.
|
|
17
105
|
*/
|
|
18
106
|
export declare function startChild(options: WorkflowOptions): Promise<string>;
|
|
@@ -45,10 +45,71 @@ function getChildInterruptPayload(context, options, execIndex) {
|
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
/**
|
|
48
|
-
* Spawns a child workflow and awaits
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
48
|
+
* Spawns a child workflow and awaits its result. The child runs as an
|
|
49
|
+
* independent job with its own lifecycle, retry policy, and dimensional
|
|
50
|
+
* isolation. If the child fails, the error is propagated to the parent
|
|
51
|
+
* as a typed error (`MemFlowFatalError`, `MemFlowMaxedError`,
|
|
52
|
+
* `MemFlowTimeoutError`, or `MemFlowRetryError`).
|
|
53
|
+
*
|
|
54
|
+
* On replay, the stored child result is returned immediately without
|
|
55
|
+
* re-spawning the child workflow.
|
|
56
|
+
*
|
|
57
|
+
* ## Child Job ID
|
|
58
|
+
*
|
|
59
|
+
* If `options.workflowId` is provided, it is used directly. Otherwise,
|
|
60
|
+
* the child ID is generated from the entity/workflow name, a GUID, the
|
|
61
|
+
* parent's dimensional coordinates, and the execution index — ensuring
|
|
62
|
+
* uniqueness across parallel and re-entrant executions.
|
|
63
|
+
*
|
|
64
|
+
* ## Examples
|
|
65
|
+
*
|
|
66
|
+
* ```typescript
|
|
67
|
+
* import { MemFlow } from '@hotmeshio/hotmesh';
|
|
68
|
+
*
|
|
69
|
+
* // Spawn a child workflow and await its result
|
|
70
|
+
* export async function parentWorkflow(orderId: string): Promise<string> {
|
|
71
|
+
* const result = await MemFlow.workflow.execChild<{ status: string }>({
|
|
72
|
+
* taskQueue: 'payments',
|
|
73
|
+
* workflowName: 'processPayment',
|
|
74
|
+
* args: [orderId, 99.99],
|
|
75
|
+
* config: {
|
|
76
|
+
* maximumAttempts: 3,
|
|
77
|
+
* backoffCoefficient: 2,
|
|
78
|
+
* },
|
|
79
|
+
* });
|
|
80
|
+
* return result.status;
|
|
81
|
+
* }
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* ```typescript
|
|
85
|
+
* // Fan-out: spawn multiple children in parallel
|
|
86
|
+
* export async function batchWorkflow(items: string[]): Promise<string[]> {
|
|
87
|
+
* const results = await Promise.all(
|
|
88
|
+
* items.map((item) =>
|
|
89
|
+
* MemFlow.workflow.execChild<string>({
|
|
90
|
+
* taskQueue: 'processors',
|
|
91
|
+
* workflowName: 'processItem',
|
|
92
|
+
* args: [item],
|
|
93
|
+
* }),
|
|
94
|
+
* ),
|
|
95
|
+
* );
|
|
96
|
+
* return results;
|
|
97
|
+
* }
|
|
98
|
+
* ```
|
|
99
|
+
*
|
|
100
|
+
* ```typescript
|
|
101
|
+
* // Entity-based child (uses entity name as task queue)
|
|
102
|
+
* const user = await MemFlow.workflow.execChild<UserRecord>({
|
|
103
|
+
* entity: 'user',
|
|
104
|
+
* args: [{ name: 'Alice', email: 'alice@example.com' }],
|
|
105
|
+
* workflowId: 'user-alice', // deterministic ID
|
|
106
|
+
* expire: 3600, // 1 hour TTL
|
|
107
|
+
* });
|
|
108
|
+
* ```
|
|
109
|
+
*
|
|
110
|
+
* @template T - The return type of the child workflow.
|
|
111
|
+
* @param {WorkflowOptions} options - Child workflow configuration.
|
|
112
|
+
* @returns {Promise<T>} The child workflow's return value.
|
|
52
113
|
*/
|
|
53
114
|
async function execChild(options) {
|
|
54
115
|
const isStartChild = options.await === false;
|
|
@@ -92,13 +153,40 @@ async function execChild(options) {
|
|
|
92
153
|
}
|
|
93
154
|
exports.execChild = execChild;
|
|
94
155
|
/**
|
|
95
|
-
* Alias for execChild.
|
|
156
|
+
* Alias for {@link execChild}.
|
|
96
157
|
*/
|
|
97
158
|
exports.executeChild = execChild;
|
|
98
159
|
/**
|
|
99
|
-
* Spawns a child workflow and
|
|
100
|
-
*
|
|
101
|
-
*
|
|
160
|
+
* Spawns a child workflow in fire-and-forget mode. The parent workflow
|
|
161
|
+
* continues immediately without waiting for the child to complete.
|
|
162
|
+
* Returns the child's job ID for later reference (e.g., to interrupt
|
|
163
|
+
* or query the child).
|
|
164
|
+
*
|
|
165
|
+
* This is a convenience wrapper around `execChild` with `await: false`.
|
|
166
|
+
*
|
|
167
|
+
* ## Example
|
|
168
|
+
*
|
|
169
|
+
* ```typescript
|
|
170
|
+
* import { MemFlow } from '@hotmeshio/hotmesh';
|
|
171
|
+
*
|
|
172
|
+
* export async function dispatchWorkflow(taskId: string): Promise<string> {
|
|
173
|
+
* // Fire-and-forget: start the child and continue immediately
|
|
174
|
+
* const childJobId = await MemFlow.workflow.startChild({
|
|
175
|
+
* taskQueue: 'background',
|
|
176
|
+
* workflowName: 'longRunningTask',
|
|
177
|
+
* args: [taskId],
|
|
178
|
+
* });
|
|
179
|
+
*
|
|
180
|
+
* // Optionally store the child ID for monitoring
|
|
181
|
+
* const search = await MemFlow.workflow.search();
|
|
182
|
+
* await search.set({ childJobId });
|
|
183
|
+
*
|
|
184
|
+
* return childJobId;
|
|
185
|
+
* }
|
|
186
|
+
* ```
|
|
187
|
+
*
|
|
188
|
+
* @param {WorkflowOptions} options - Child workflow configuration.
|
|
189
|
+
* @returns {Promise<string>} The child workflow's job ID.
|
|
102
190
|
*/
|
|
103
191
|
async function startChild(options) {
|
|
104
192
|
return execChild({ ...options, await: false });
|
|
@@ -1,65 +1,80 @@
|
|
|
1
1
|
import { HookOptions } from './common';
|
|
2
2
|
/**
|
|
3
|
-
* Extended hook options that include signal configuration
|
|
3
|
+
* Extended hook options that include signal configuration.
|
|
4
|
+
* Used by `execHook()` and `execHookBatch()`.
|
|
4
5
|
*/
|
|
5
6
|
export interface ExecHookOptions extends HookOptions {
|
|
6
7
|
/** Signal ID to send after hook execution; if not provided, a random one will be generated */
|
|
7
8
|
signalId?: string;
|
|
8
9
|
}
|
|
9
10
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
11
|
+
* Combines `hook()` + `waitFor()` into a single call: spawns a hook
|
|
12
|
+
* function on a target workflow and suspends the current workflow until
|
|
13
|
+
* the hook signals completion. This is the recommended pattern for
|
|
14
|
+
* request/response communication between workflow threads.
|
|
12
15
|
*
|
|
13
|
-
*
|
|
14
|
-
* to the hooked function. The hooked function should check for this signal parameter
|
|
15
|
-
* and emit the signal when processing is complete.
|
|
16
|
+
* ## Signal Injection
|
|
16
17
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
18
|
+
* A `signalId` is automatically generated (or use the one you provide)
|
|
19
|
+
* and injected as the **last argument** to the hooked function as
|
|
20
|
+
* `{ signal: string, $memflow: true }`. The hook function must call
|
|
21
|
+
* `MemFlow.workflow.signal(signalInfo.signal, result)` to deliver
|
|
22
|
+
* its response back to the waiting workflow.
|
|
19
23
|
*
|
|
20
|
-
*
|
|
21
|
-
* @param {ExecHookOptions} options - Hook configuration with signal ID.
|
|
22
|
-
* @returns {Promise<T>} The signal result from the hooked function.
|
|
24
|
+
* ## Difference from `execChild`
|
|
23
25
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* taskQueue: 'processing',
|
|
29
|
-
* workflowName: 'processData',
|
|
30
|
-
* args: ['user123', 'batch-process'],
|
|
31
|
-
* signalId: 'processing-complete'
|
|
32
|
-
* });
|
|
26
|
+
* - `execChild` spawns a **new** workflow job with its own lifecycle.
|
|
27
|
+
* - `execHook` runs within an **existing** workflow's job, in an
|
|
28
|
+
* isolated dimensional thread. This is lighter-weight and shares
|
|
29
|
+
* the parent job's data namespace.
|
|
33
30
|
*
|
|
34
|
-
*
|
|
35
|
-
* export async function processData(userId: string, processType: string, signalInfo?: { signal: string }) {
|
|
36
|
-
* // ... do processing work ...
|
|
37
|
-
* const result = { userId, processType, status: 'completed' };
|
|
31
|
+
* ## Examples
|
|
38
32
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* await MemFlow.workflow.signal(signalInfo.signal, result);
|
|
42
|
-
* }
|
|
33
|
+
* ```typescript
|
|
34
|
+
* import { MemFlow } from '@hotmeshio/hotmesh';
|
|
43
35
|
*
|
|
44
|
-
*
|
|
36
|
+
* // Orchestrator: spawn a hook and await its result
|
|
37
|
+
* export async function reviewWorkflow(docId: string): Promise<string> {
|
|
38
|
+
* const verdict = await MemFlow.workflow.execHook<{ approved: boolean }>({
|
|
39
|
+
* taskQueue: 'reviewers',
|
|
40
|
+
* workflowName: 'reviewDocument',
|
|
41
|
+
* args: [docId],
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* return verdict.approved ? 'accepted' : 'rejected';
|
|
45
45
|
* }
|
|
46
46
|
* ```
|
|
47
47
|
*
|
|
48
|
-
* @example
|
|
49
48
|
* ```typescript
|
|
50
|
-
* //
|
|
51
|
-
* export async function
|
|
52
|
-
*
|
|
53
|
-
*
|
|
49
|
+
* // The hooked function (runs on the 'reviewers' worker)
|
|
50
|
+
* export async function reviewDocument(
|
|
51
|
+
* docId: string,
|
|
52
|
+
* signalInfo?: { signal: string; $memflow: boolean },
|
|
53
|
+
* ): Promise<{ approved: boolean }> {
|
|
54
|
+
* const { analyzeDocument } = MemFlow.workflow.proxyActivities<typeof activities>();
|
|
55
|
+
* const score = await analyzeDocument(docId);
|
|
56
|
+
* const result = { approved: score > 0.8 };
|
|
54
57
|
*
|
|
55
|
-
* //
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* await MemFlow.workflow.signal(lastArg.signal, result);
|
|
58
|
+
* // Signal the waiting workflow with the result
|
|
59
|
+
* if (signalInfo?.signal) {
|
|
60
|
+
* await MemFlow.workflow.signal(signalInfo.signal, result);
|
|
59
61
|
* }
|
|
60
|
-
*
|
|
61
62
|
* return result;
|
|
62
63
|
* }
|
|
63
64
|
* ```
|
|
65
|
+
*
|
|
66
|
+
* ```typescript
|
|
67
|
+
* // With explicit signalId for traceability
|
|
68
|
+
* const result = await MemFlow.workflow.execHook<AnalysisResult>({
|
|
69
|
+
* taskQueue: 'analyzers',
|
|
70
|
+
* workflowName: 'runAnalysis',
|
|
71
|
+
* args: [datasetId],
|
|
72
|
+
* signalId: `analysis-${datasetId}`,
|
|
73
|
+
* });
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* @template T - The type of data returned by the hook function's signal.
|
|
77
|
+
* @param {ExecHookOptions} options - Hook configuration including target workflow and arguments.
|
|
78
|
+
* @returns {Promise<T>} The signal result from the hooked function.
|
|
64
79
|
*/
|
|
65
80
|
export declare function execHook<T>(options: ExecHookOptions): Promise<T>;
|
|
@@ -5,60 +5,74 @@ const hook_1 = require("./hook");
|
|
|
5
5
|
const waitFor_1 = require("./waitFor");
|
|
6
6
|
const interruption_1 = require("./interruption");
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* Combines `hook()` + `waitFor()` into a single call: spawns a hook
|
|
9
|
+
* function on a target workflow and suspends the current workflow until
|
|
10
|
+
* the hook signals completion. This is the recommended pattern for
|
|
11
|
+
* request/response communication between workflow threads.
|
|
10
12
|
*
|
|
11
|
-
*
|
|
12
|
-
* to the hooked function. The hooked function should check for this signal parameter
|
|
13
|
-
* and emit the signal when processing is complete.
|
|
13
|
+
* ## Signal Injection
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* A `signalId` is automatically generated (or use the one you provide)
|
|
16
|
+
* and injected as the **last argument** to the hooked function as
|
|
17
|
+
* `{ signal: string, $memflow: true }`. The hook function must call
|
|
18
|
+
* `MemFlow.workflow.signal(signalInfo.signal, result)` to deliver
|
|
19
|
+
* its response back to the waiting workflow.
|
|
17
20
|
*
|
|
18
|
-
*
|
|
19
|
-
* @param {ExecHookOptions} options - Hook configuration with signal ID.
|
|
20
|
-
* @returns {Promise<T>} The signal result from the hooked function.
|
|
21
|
+
* ## Difference from `execChild`
|
|
21
22
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* taskQueue: 'processing',
|
|
27
|
-
* workflowName: 'processData',
|
|
28
|
-
* args: ['user123', 'batch-process'],
|
|
29
|
-
* signalId: 'processing-complete'
|
|
30
|
-
* });
|
|
23
|
+
* - `execChild` spawns a **new** workflow job with its own lifecycle.
|
|
24
|
+
* - `execHook` runs within an **existing** workflow's job, in an
|
|
25
|
+
* isolated dimensional thread. This is lighter-weight and shares
|
|
26
|
+
* the parent job's data namespace.
|
|
31
27
|
*
|
|
32
|
-
*
|
|
33
|
-
* export async function processData(userId: string, processType: string, signalInfo?: { signal: string }) {
|
|
34
|
-
* // ... do processing work ...
|
|
35
|
-
* const result = { userId, processType, status: 'completed' };
|
|
28
|
+
* ## Examples
|
|
36
29
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* await MemFlow.workflow.signal(signalInfo.signal, result);
|
|
40
|
-
* }
|
|
30
|
+
* ```typescript
|
|
31
|
+
* import { MemFlow } from '@hotmeshio/hotmesh';
|
|
41
32
|
*
|
|
42
|
-
*
|
|
33
|
+
* // Orchestrator: spawn a hook and await its result
|
|
34
|
+
* export async function reviewWorkflow(docId: string): Promise<string> {
|
|
35
|
+
* const verdict = await MemFlow.workflow.execHook<{ approved: boolean }>({
|
|
36
|
+
* taskQueue: 'reviewers',
|
|
37
|
+
* workflowName: 'reviewDocument',
|
|
38
|
+
* args: [docId],
|
|
39
|
+
* });
|
|
40
|
+
*
|
|
41
|
+
* return verdict.approved ? 'accepted' : 'rejected';
|
|
43
42
|
* }
|
|
44
43
|
* ```
|
|
45
44
|
*
|
|
46
|
-
* @example
|
|
47
45
|
* ```typescript
|
|
48
|
-
* //
|
|
49
|
-
* export async function
|
|
50
|
-
*
|
|
51
|
-
*
|
|
46
|
+
* // The hooked function (runs on the 'reviewers' worker)
|
|
47
|
+
* export async function reviewDocument(
|
|
48
|
+
* docId: string,
|
|
49
|
+
* signalInfo?: { signal: string; $memflow: boolean },
|
|
50
|
+
* ): Promise<{ approved: boolean }> {
|
|
51
|
+
* const { analyzeDocument } = MemFlow.workflow.proxyActivities<typeof activities>();
|
|
52
|
+
* const score = await analyzeDocument(docId);
|
|
53
|
+
* const result = { approved: score > 0.8 };
|
|
52
54
|
*
|
|
53
|
-
* //
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* await MemFlow.workflow.signal(lastArg.signal, result);
|
|
55
|
+
* // Signal the waiting workflow with the result
|
|
56
|
+
* if (signalInfo?.signal) {
|
|
57
|
+
* await MemFlow.workflow.signal(signalInfo.signal, result);
|
|
57
58
|
* }
|
|
58
|
-
*
|
|
59
59
|
* return result;
|
|
60
60
|
* }
|
|
61
61
|
* ```
|
|
62
|
+
*
|
|
63
|
+
* ```typescript
|
|
64
|
+
* // With explicit signalId for traceability
|
|
65
|
+
* const result = await MemFlow.workflow.execHook<AnalysisResult>({
|
|
66
|
+
* taskQueue: 'analyzers',
|
|
67
|
+
* workflowName: 'runAnalysis',
|
|
68
|
+
* args: [datasetId],
|
|
69
|
+
* signalId: `analysis-${datasetId}`,
|
|
70
|
+
* });
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* @template T - The type of data returned by the hook function's signal.
|
|
74
|
+
* @param {ExecHookOptions} options - Hook configuration including target workflow and arguments.
|
|
75
|
+
* @returns {Promise<T>} The signal result from the hooked function.
|
|
62
76
|
*/
|
|
63
77
|
async function execHook(options) {
|
|
64
78
|
try {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ExecHookOptions } from './execHook';
|
|
2
2
|
/**
|
|
3
|
-
* Configuration for a single hook in a batch execution
|
|
3
|
+
* Configuration for a single hook in a batch execution.
|
|
4
|
+
* @see {@link execHookBatch}
|
|
4
5
|
*/
|
|
5
6
|
export interface BatchHookConfig<T = any> {
|
|
6
7
|
/** Unique key to identify this hook's result in the returned object */
|
|
@@ -9,46 +10,98 @@ export interface BatchHookConfig<T = any> {
|
|
|
9
10
|
options: ExecHookOptions;
|
|
10
11
|
}
|
|
11
12
|
/**
|
|
12
|
-
* Executes multiple hooks in parallel and awaits all their signal responses
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* Executes multiple hooks in parallel and awaits all their signal responses,
|
|
14
|
+
* returning a keyed object of results. This is the recommended way to run
|
|
15
|
+
* concurrent hooks — it solves a race condition where calling
|
|
16
|
+
* `Promise.all([execHook(), execHook()])` would throw before all `waitFor`
|
|
17
|
+
* registrations complete.
|
|
15
18
|
*
|
|
16
|
-
*
|
|
17
|
-
* preventing signals from being sent before the framework is ready to receive them.
|
|
19
|
+
* ## Three-Phase Execution
|
|
18
20
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
21
|
+
* 1. **Fire all hooks** via `Promise.all` (registers streams immediately).
|
|
22
|
+
* 2. **Await all signals** via `Promise.all` (all `waitFor` registrations
|
|
23
|
+
* happen together before any `MemFlowWaitForError` is thrown).
|
|
24
|
+
* 3. **Combine results** into a `{ [key]: result }` map.
|
|
25
|
+
*
|
|
26
|
+
* ## Examples
|
|
27
|
+
*
|
|
28
|
+
* ```typescript
|
|
29
|
+
* import { MemFlow } from '@hotmeshio/hotmesh';
|
|
30
|
+
*
|
|
31
|
+
* // Fan-out to multiple AI agents, gather all perspectives
|
|
32
|
+
* export async function researchWorkflow(query: string): Promise<Summary> {
|
|
33
|
+
* const perspectives = await MemFlow.workflow.execHookBatch<{
|
|
34
|
+
* optimistic: PerspectiveResult;
|
|
35
|
+
* skeptical: PerspectiveResult;
|
|
36
|
+
* neutral: PerspectiveResult;
|
|
37
|
+
* }>([
|
|
38
|
+
* {
|
|
39
|
+
* key: 'optimistic',
|
|
40
|
+
* options: {
|
|
41
|
+
* taskQueue: 'agents',
|
|
42
|
+
* workflowName: 'analyzeOptimistic',
|
|
43
|
+
* args: [query],
|
|
44
|
+
* },
|
|
45
|
+
* },
|
|
46
|
+
* {
|
|
47
|
+
* key: 'skeptical',
|
|
48
|
+
* options: {
|
|
49
|
+
* taskQueue: 'agents',
|
|
50
|
+
* workflowName: 'analyzeSkeptical',
|
|
51
|
+
* args: [query],
|
|
52
|
+
* },
|
|
53
|
+
* },
|
|
54
|
+
* {
|
|
55
|
+
* key: 'neutral',
|
|
56
|
+
* options: {
|
|
57
|
+
* taskQueue: 'agents',
|
|
58
|
+
* workflowName: 'analyzeNeutral',
|
|
59
|
+
* args: [query],
|
|
60
|
+
* },
|
|
61
|
+
* },
|
|
62
|
+
* ]);
|
|
63
|
+
*
|
|
64
|
+
* // All three results are available as typed properties
|
|
65
|
+
* const { synthesize } = MemFlow.workflow.proxyActivities<typeof activities>();
|
|
66
|
+
* return await synthesize(
|
|
67
|
+
* perspectives.optimistic,
|
|
68
|
+
* perspectives.skeptical,
|
|
69
|
+
* perspectives.neutral,
|
|
70
|
+
* );
|
|
71
|
+
* }
|
|
72
|
+
* ```
|
|
22
73
|
*
|
|
23
|
-
* @example
|
|
24
74
|
* ```typescript
|
|
25
|
-
* //
|
|
26
|
-
* const
|
|
27
|
-
*
|
|
28
|
-
*
|
|
75
|
+
* // Parallel validation with different services
|
|
76
|
+
* const checks = await MemFlow.workflow.execHookBatch<{
|
|
77
|
+
* fraud: { safe: boolean };
|
|
78
|
+
* compliance: { approved: boolean };
|
|
29
79
|
* }>([
|
|
30
80
|
* {
|
|
31
|
-
* key: '
|
|
81
|
+
* key: 'fraud',
|
|
32
82
|
* options: {
|
|
33
|
-
* taskQueue: '
|
|
34
|
-
* workflowName: '
|
|
35
|
-
* args: [
|
|
36
|
-
*
|
|
37
|
-
* }
|
|
83
|
+
* taskQueue: 'fraud-detection',
|
|
84
|
+
* workflowName: 'checkFraud',
|
|
85
|
+
* args: [transactionId],
|
|
86
|
+
* },
|
|
38
87
|
* },
|
|
39
88
|
* {
|
|
40
|
-
* key: '
|
|
89
|
+
* key: 'compliance',
|
|
41
90
|
* options: {
|
|
42
|
-
* taskQueue: '
|
|
43
|
-
* workflowName: '
|
|
44
|
-
* args: [
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* }
|
|
91
|
+
* taskQueue: 'compliance',
|
|
92
|
+
* workflowName: 'checkCompliance',
|
|
93
|
+
* args: [transactionId],
|
|
94
|
+
* },
|
|
95
|
+
* },
|
|
48
96
|
* ]);
|
|
49
97
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
98
|
+
* if (checks.fraud.safe && checks.compliance.approved) {
|
|
99
|
+
* // proceed with transaction
|
|
100
|
+
* }
|
|
52
101
|
* ```
|
|
102
|
+
*
|
|
103
|
+
* @template T - Object type with keys matching the batch hook keys.
|
|
104
|
+
* @param {BatchHookConfig[]} hookConfigs - Array of hook configurations with unique keys.
|
|
105
|
+
* @returns {Promise<T>} Object mapping each config's `key` to its signal response.
|
|
53
106
|
*/
|
|
54
107
|
export declare function execHookBatch<T extends Record<string, any>>(hookConfigs: BatchHookConfig[]): Promise<T>;
|