@hotmeshio/hotmesh 0.5.5 → 0.5.6
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 +7 -19
- package/build/package.json +3 -2
- package/build/services/activities/trigger.js +1 -1
- package/build/services/connector/factory.js +2 -1
- package/build/services/connector/providers/postgres.js +11 -6
- package/build/services/memflow/client.js +4 -2
- package/build/services/memflow/index.d.ts +154 -34
- package/build/services/memflow/index.js +165 -33
- package/build/services/memflow/interceptor.d.ts +241 -0
- package/build/services/memflow/interceptor.js +256 -0
- package/build/services/memflow/worker.js +10 -1
- package/build/services/memflow/workflow/execChild.js +3 -1
- package/build/services/memflow/workflow/execHook.js +1 -1
- package/build/services/memflow/workflow/hook.js +4 -2
- package/build/services/memflow/workflow/proxyActivities.js +2 -1
- package/build/services/router/consumption/index.js +23 -9
- package/build/services/router/error-handling/index.js +3 -3
- package/build/services/search/providers/postgres/postgres.js +47 -19
- package/build/services/store/providers/postgres/kvtypes/hash/basic.js +1 -1
- package/build/services/store/providers/postgres/kvtypes/hash/index.js +2 -2
- package/build/services/store/providers/postgres/kvtypes/hash/jsonb.js +11 -11
- package/build/services/store/providers/postgres/postgres.js +8 -8
- package/build/services/stream/providers/postgres/postgres.js +23 -20
- package/build/services/sub/providers/postgres/postgres.js +11 -3
- package/build/services/task/index.js +4 -4
- package/build/types/memflow.d.ts +78 -0
- package/package.json +3 -2
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { WorkflowInterceptor, InterceptorRegistry } from '../../types/memflow';
|
|
2
|
+
/**
|
|
3
|
+
* Service for managing workflow interceptors that wrap workflow execution
|
|
4
|
+
* in an onion-like pattern. Each interceptor can perform actions before
|
|
5
|
+
* and after workflow execution, add cross-cutting concerns, and handle errors.
|
|
6
|
+
*
|
|
7
|
+
* ## Basic Interceptor Pattern
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* // Create and configure interceptors
|
|
12
|
+
* const service = new InterceptorService();
|
|
13
|
+
*
|
|
14
|
+
* // Add logging interceptor (outermost)
|
|
15
|
+
* service.register({
|
|
16
|
+
* async execute(ctx, next) {
|
|
17
|
+
* console.log('Starting workflow');
|
|
18
|
+
* const result = await next();
|
|
19
|
+
* console.log('Workflow completed');
|
|
20
|
+
* return result;
|
|
21
|
+
* }
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* // Add metrics interceptor (middle)
|
|
25
|
+
* service.register({
|
|
26
|
+
* async execute(ctx, next) {
|
|
27
|
+
* const timer = startTimer();
|
|
28
|
+
* const result = await next();
|
|
29
|
+
* recordDuration(timer.end());
|
|
30
|
+
* return result;
|
|
31
|
+
* }
|
|
32
|
+
* });
|
|
33
|
+
*
|
|
34
|
+
* // Add error handling interceptor (innermost)
|
|
35
|
+
* service.register({
|
|
36
|
+
* async execute(ctx, next) {
|
|
37
|
+
* try {
|
|
38
|
+
* return await next();
|
|
39
|
+
* } catch (err) {
|
|
40
|
+
* reportError(err);
|
|
41
|
+
* throw err;
|
|
42
|
+
* }
|
|
43
|
+
* }
|
|
44
|
+
* });
|
|
45
|
+
*
|
|
46
|
+
* // Execute workflow through interceptor chain
|
|
47
|
+
* const result = await service.executeChain(context, async () => {
|
|
48
|
+
* return await workflowFn();
|
|
49
|
+
* });
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* ## Durable Interceptors with MemFlow Functions
|
|
53
|
+
*
|
|
54
|
+
* Interceptors run within the workflow's async local storage context, which means
|
|
55
|
+
* they can use MemFlow functions like `sleepFor`, `entity`, `proxyActivities`, etc.
|
|
56
|
+
* These interceptors participate in the HotMesh interruption/replay pattern.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* import { MemFlow } from '@hotmeshio/hotmesh';
|
|
61
|
+
*
|
|
62
|
+
* // Rate limiting interceptor that sleeps before execution
|
|
63
|
+
* const rateLimitInterceptor: WorkflowInterceptor = {
|
|
64
|
+
* async execute(ctx, next) {
|
|
65
|
+
* try {
|
|
66
|
+
* // This sleep will cause an interruption on first execution
|
|
67
|
+
* await MemFlow.workflow.sleepFor('1 second');
|
|
68
|
+
*
|
|
69
|
+
* const result = await next();
|
|
70
|
+
*
|
|
71
|
+
* // Another sleep after workflow completes
|
|
72
|
+
* await MemFlow.workflow.sleepFor('500 milliseconds');
|
|
73
|
+
*
|
|
74
|
+
* return result;
|
|
75
|
+
* } catch (err) {
|
|
76
|
+
* // CRITICAL: Always check for HotMesh interruptions
|
|
77
|
+
* if (MemFlow.didInterrupt(err)) {
|
|
78
|
+
* throw err; // Rethrow interruptions for replay system
|
|
79
|
+
* }
|
|
80
|
+
* // Handle actual errors
|
|
81
|
+
* console.error('Interceptor error:', err);
|
|
82
|
+
* throw err;
|
|
83
|
+
* }
|
|
84
|
+
* }
|
|
85
|
+
* };
|
|
86
|
+
*
|
|
87
|
+
* // Entity-based audit interceptor
|
|
88
|
+
* const auditInterceptor: WorkflowInterceptor = {
|
|
89
|
+
* async execute(ctx, next) {
|
|
90
|
+
* try {
|
|
91
|
+
* const entity = await MemFlow.workflow.entity();
|
|
92
|
+
* await entity.append('auditLog', {
|
|
93
|
+
* action: 'workflow_started',
|
|
94
|
+
* timestamp: new Date().toISOString(),
|
|
95
|
+
* workflowId: ctx.get('workflowId')
|
|
96
|
+
* });
|
|
97
|
+
*
|
|
98
|
+
* const startTime = Date.now();
|
|
99
|
+
* const result = await next();
|
|
100
|
+
* const duration = Date.now() - startTime;
|
|
101
|
+
*
|
|
102
|
+
* await entity.append('auditLog', {
|
|
103
|
+
* action: 'workflow_completed',
|
|
104
|
+
* timestamp: new Date().toISOString(),
|
|
105
|
+
* duration,
|
|
106
|
+
* success: true
|
|
107
|
+
* });
|
|
108
|
+
*
|
|
109
|
+
* return result;
|
|
110
|
+
* } catch (err) {
|
|
111
|
+
* if (MemFlow.didInterrupt(err)) {
|
|
112
|
+
* throw err;
|
|
113
|
+
* }
|
|
114
|
+
*
|
|
115
|
+
* // Log failure to entity
|
|
116
|
+
* const entity = await MemFlow.workflow.entity();
|
|
117
|
+
* await entity.append('auditLog', {
|
|
118
|
+
* action: 'workflow_failed',
|
|
119
|
+
* timestamp: new Date().toISOString(),
|
|
120
|
+
* error: err.message
|
|
121
|
+
* });
|
|
122
|
+
*
|
|
123
|
+
* throw err;
|
|
124
|
+
* }
|
|
125
|
+
* }
|
|
126
|
+
* };
|
|
127
|
+
*
|
|
128
|
+
* // Register interceptors
|
|
129
|
+
* MemFlow.registerInterceptor(rateLimitInterceptor);
|
|
130
|
+
* MemFlow.registerInterceptor(auditInterceptor);
|
|
131
|
+
* ```
|
|
132
|
+
*
|
|
133
|
+
* ## Execution Pattern with Interruptions
|
|
134
|
+
*
|
|
135
|
+
* When interceptors use MemFlow functions, the workflow will execute multiple times
|
|
136
|
+
* due to the interruption/replay pattern:
|
|
137
|
+
*
|
|
138
|
+
* 1. **First execution**: Interceptor calls `sleepFor` → throws `MemFlowSleepError` → workflow pauses
|
|
139
|
+
* 2. **Second execution**: Interceptor sleep replays (skipped), workflow runs → proxy activity throws `MemFlowProxyError` → workflow pauses
|
|
140
|
+
* 3. **Third execution**: All previous operations replay, interceptor sleep after workflow → throws `MemFlowSleepError` → workflow pauses
|
|
141
|
+
* 4. **Fourth execution**: Everything replays successfully, workflow completes
|
|
142
|
+
*
|
|
143
|
+
* This pattern ensures deterministic, durable execution across all interceptors and workflow code.
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```typescript
|
|
147
|
+
* // Interceptor with complex MemFlow operations
|
|
148
|
+
* const complexInterceptor: WorkflowInterceptor = {
|
|
149
|
+
* async execute(ctx, next) {
|
|
150
|
+
* try {
|
|
151
|
+
* // Get persistent state
|
|
152
|
+
* const entity = await MemFlow.workflow.entity();
|
|
153
|
+
* const state = await entity.get() as any;
|
|
154
|
+
*
|
|
155
|
+
* // Conditional durable operations
|
|
156
|
+
* if (!state.preProcessed) {
|
|
157
|
+
* await MemFlow.workflow.sleepFor('100 milliseconds');
|
|
158
|
+
* await entity.merge({ preProcessed: true });
|
|
159
|
+
* }
|
|
160
|
+
*
|
|
161
|
+
* // Execute the workflow
|
|
162
|
+
* const result = await next();
|
|
163
|
+
*
|
|
164
|
+
* // Post-processing with child workflow
|
|
165
|
+
* if (!state.postProcessed) {
|
|
166
|
+
* await MemFlow.workflow.execChild({
|
|
167
|
+
* taskQueue: 'cleanup',
|
|
168
|
+
* workflowName: 'cleanupWorkflow',
|
|
169
|
+
* args: [result]
|
|
170
|
+
* });
|
|
171
|
+
* await entity.merge({ postProcessed: true });
|
|
172
|
+
* }
|
|
173
|
+
*
|
|
174
|
+
* return result;
|
|
175
|
+
* } catch (err) {
|
|
176
|
+
* if (MemFlow.didInterrupt(err)) {
|
|
177
|
+
* throw err;
|
|
178
|
+
* }
|
|
179
|
+
* throw err;
|
|
180
|
+
* }
|
|
181
|
+
* }
|
|
182
|
+
* };
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
export declare class InterceptorService implements InterceptorRegistry {
|
|
186
|
+
interceptors: WorkflowInterceptor[];
|
|
187
|
+
/**
|
|
188
|
+
* Register a new workflow interceptor that will wrap workflow execution.
|
|
189
|
+
* Interceptors are executed in the order they are registered, with the
|
|
190
|
+
* first registered interceptor being the outermost wrapper.
|
|
191
|
+
*
|
|
192
|
+
* @param interceptor The interceptor to register
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```typescript
|
|
196
|
+
* service.register({
|
|
197
|
+
* async execute(ctx, next) {
|
|
198
|
+
* console.time('workflow');
|
|
199
|
+
* try {
|
|
200
|
+
* return await next();
|
|
201
|
+
* } finally {
|
|
202
|
+
* console.timeEnd('workflow');
|
|
203
|
+
* }
|
|
204
|
+
* }
|
|
205
|
+
* });
|
|
206
|
+
* ```
|
|
207
|
+
*/
|
|
208
|
+
register(interceptor: WorkflowInterceptor): void;
|
|
209
|
+
/**
|
|
210
|
+
* Execute the interceptor chain around the workflow function.
|
|
211
|
+
* The chain is built in an onion pattern where each interceptor
|
|
212
|
+
* wraps the next one, with the workflow function at the center.
|
|
213
|
+
*
|
|
214
|
+
* @param ctx The workflow context map
|
|
215
|
+
* @param fn The workflow function to execute
|
|
216
|
+
* @returns The result of the workflow execution
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* ```typescript
|
|
220
|
+
* // Execute workflow with timing
|
|
221
|
+
* const result = await service.executeChain(context, async () => {
|
|
222
|
+
* const start = Date.now();
|
|
223
|
+
* try {
|
|
224
|
+
* return await workflowFn();
|
|
225
|
+
* } finally {
|
|
226
|
+
* console.log('Duration:', Date.now() - start);
|
|
227
|
+
* }
|
|
228
|
+
* });
|
|
229
|
+
* ```
|
|
230
|
+
*/
|
|
231
|
+
executeChain(ctx: Map<string, any>, fn: () => Promise<any>): Promise<any>;
|
|
232
|
+
/**
|
|
233
|
+
* Clear all registered interceptors.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* ```typescript
|
|
237
|
+
* service.clear();
|
|
238
|
+
* ```
|
|
239
|
+
*/
|
|
240
|
+
clear(): void;
|
|
241
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InterceptorService = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Service for managing workflow interceptors that wrap workflow execution
|
|
6
|
+
* in an onion-like pattern. Each interceptor can perform actions before
|
|
7
|
+
* and after workflow execution, add cross-cutting concerns, and handle errors.
|
|
8
|
+
*
|
|
9
|
+
* ## Basic Interceptor Pattern
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* // Create and configure interceptors
|
|
14
|
+
* const service = new InterceptorService();
|
|
15
|
+
*
|
|
16
|
+
* // Add logging interceptor (outermost)
|
|
17
|
+
* service.register({
|
|
18
|
+
* async execute(ctx, next) {
|
|
19
|
+
* console.log('Starting workflow');
|
|
20
|
+
* const result = await next();
|
|
21
|
+
* console.log('Workflow completed');
|
|
22
|
+
* return result;
|
|
23
|
+
* }
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* // Add metrics interceptor (middle)
|
|
27
|
+
* service.register({
|
|
28
|
+
* async execute(ctx, next) {
|
|
29
|
+
* const timer = startTimer();
|
|
30
|
+
* const result = await next();
|
|
31
|
+
* recordDuration(timer.end());
|
|
32
|
+
* return result;
|
|
33
|
+
* }
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* // Add error handling interceptor (innermost)
|
|
37
|
+
* service.register({
|
|
38
|
+
* async execute(ctx, next) {
|
|
39
|
+
* try {
|
|
40
|
+
* return await next();
|
|
41
|
+
* } catch (err) {
|
|
42
|
+
* reportError(err);
|
|
43
|
+
* throw err;
|
|
44
|
+
* }
|
|
45
|
+
* }
|
|
46
|
+
* });
|
|
47
|
+
*
|
|
48
|
+
* // Execute workflow through interceptor chain
|
|
49
|
+
* const result = await service.executeChain(context, async () => {
|
|
50
|
+
* return await workflowFn();
|
|
51
|
+
* });
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* ## Durable Interceptors with MemFlow Functions
|
|
55
|
+
*
|
|
56
|
+
* Interceptors run within the workflow's async local storage context, which means
|
|
57
|
+
* they can use MemFlow functions like `sleepFor`, `entity`, `proxyActivities`, etc.
|
|
58
|
+
* These interceptors participate in the HotMesh interruption/replay pattern.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* import { MemFlow } from '@hotmeshio/hotmesh';
|
|
63
|
+
*
|
|
64
|
+
* // Rate limiting interceptor that sleeps before execution
|
|
65
|
+
* const rateLimitInterceptor: WorkflowInterceptor = {
|
|
66
|
+
* async execute(ctx, next) {
|
|
67
|
+
* try {
|
|
68
|
+
* // This sleep will cause an interruption on first execution
|
|
69
|
+
* await MemFlow.workflow.sleepFor('1 second');
|
|
70
|
+
*
|
|
71
|
+
* const result = await next();
|
|
72
|
+
*
|
|
73
|
+
* // Another sleep after workflow completes
|
|
74
|
+
* await MemFlow.workflow.sleepFor('500 milliseconds');
|
|
75
|
+
*
|
|
76
|
+
* return result;
|
|
77
|
+
* } catch (err) {
|
|
78
|
+
* // CRITICAL: Always check for HotMesh interruptions
|
|
79
|
+
* if (MemFlow.didInterrupt(err)) {
|
|
80
|
+
* throw err; // Rethrow interruptions for replay system
|
|
81
|
+
* }
|
|
82
|
+
* // Handle actual errors
|
|
83
|
+
* console.error('Interceptor error:', err);
|
|
84
|
+
* throw err;
|
|
85
|
+
* }
|
|
86
|
+
* }
|
|
87
|
+
* };
|
|
88
|
+
*
|
|
89
|
+
* // Entity-based audit interceptor
|
|
90
|
+
* const auditInterceptor: WorkflowInterceptor = {
|
|
91
|
+
* async execute(ctx, next) {
|
|
92
|
+
* try {
|
|
93
|
+
* const entity = await MemFlow.workflow.entity();
|
|
94
|
+
* await entity.append('auditLog', {
|
|
95
|
+
* action: 'workflow_started',
|
|
96
|
+
* timestamp: new Date().toISOString(),
|
|
97
|
+
* workflowId: ctx.get('workflowId')
|
|
98
|
+
* });
|
|
99
|
+
*
|
|
100
|
+
* const startTime = Date.now();
|
|
101
|
+
* const result = await next();
|
|
102
|
+
* const duration = Date.now() - startTime;
|
|
103
|
+
*
|
|
104
|
+
* await entity.append('auditLog', {
|
|
105
|
+
* action: 'workflow_completed',
|
|
106
|
+
* timestamp: new Date().toISOString(),
|
|
107
|
+
* duration,
|
|
108
|
+
* success: true
|
|
109
|
+
* });
|
|
110
|
+
*
|
|
111
|
+
* return result;
|
|
112
|
+
* } catch (err) {
|
|
113
|
+
* if (MemFlow.didInterrupt(err)) {
|
|
114
|
+
* throw err;
|
|
115
|
+
* }
|
|
116
|
+
*
|
|
117
|
+
* // Log failure to entity
|
|
118
|
+
* const entity = await MemFlow.workflow.entity();
|
|
119
|
+
* await entity.append('auditLog', {
|
|
120
|
+
* action: 'workflow_failed',
|
|
121
|
+
* timestamp: new Date().toISOString(),
|
|
122
|
+
* error: err.message
|
|
123
|
+
* });
|
|
124
|
+
*
|
|
125
|
+
* throw err;
|
|
126
|
+
* }
|
|
127
|
+
* }
|
|
128
|
+
* };
|
|
129
|
+
*
|
|
130
|
+
* // Register interceptors
|
|
131
|
+
* MemFlow.registerInterceptor(rateLimitInterceptor);
|
|
132
|
+
* MemFlow.registerInterceptor(auditInterceptor);
|
|
133
|
+
* ```
|
|
134
|
+
*
|
|
135
|
+
* ## Execution Pattern with Interruptions
|
|
136
|
+
*
|
|
137
|
+
* When interceptors use MemFlow functions, the workflow will execute multiple times
|
|
138
|
+
* due to the interruption/replay pattern:
|
|
139
|
+
*
|
|
140
|
+
* 1. **First execution**: Interceptor calls `sleepFor` → throws `MemFlowSleepError` → workflow pauses
|
|
141
|
+
* 2. **Second execution**: Interceptor sleep replays (skipped), workflow runs → proxy activity throws `MemFlowProxyError` → workflow pauses
|
|
142
|
+
* 3. **Third execution**: All previous operations replay, interceptor sleep after workflow → throws `MemFlowSleepError` → workflow pauses
|
|
143
|
+
* 4. **Fourth execution**: Everything replays successfully, workflow completes
|
|
144
|
+
*
|
|
145
|
+
* This pattern ensures deterministic, durable execution across all interceptors and workflow code.
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```typescript
|
|
149
|
+
* // Interceptor with complex MemFlow operations
|
|
150
|
+
* const complexInterceptor: WorkflowInterceptor = {
|
|
151
|
+
* async execute(ctx, next) {
|
|
152
|
+
* try {
|
|
153
|
+
* // Get persistent state
|
|
154
|
+
* const entity = await MemFlow.workflow.entity();
|
|
155
|
+
* const state = await entity.get() as any;
|
|
156
|
+
*
|
|
157
|
+
* // Conditional durable operations
|
|
158
|
+
* if (!state.preProcessed) {
|
|
159
|
+
* await MemFlow.workflow.sleepFor('100 milliseconds');
|
|
160
|
+
* await entity.merge({ preProcessed: true });
|
|
161
|
+
* }
|
|
162
|
+
*
|
|
163
|
+
* // Execute the workflow
|
|
164
|
+
* const result = await next();
|
|
165
|
+
*
|
|
166
|
+
* // Post-processing with child workflow
|
|
167
|
+
* if (!state.postProcessed) {
|
|
168
|
+
* await MemFlow.workflow.execChild({
|
|
169
|
+
* taskQueue: 'cleanup',
|
|
170
|
+
* workflowName: 'cleanupWorkflow',
|
|
171
|
+
* args: [result]
|
|
172
|
+
* });
|
|
173
|
+
* await entity.merge({ postProcessed: true });
|
|
174
|
+
* }
|
|
175
|
+
*
|
|
176
|
+
* return result;
|
|
177
|
+
* } catch (err) {
|
|
178
|
+
* if (MemFlow.didInterrupt(err)) {
|
|
179
|
+
* throw err;
|
|
180
|
+
* }
|
|
181
|
+
* throw err;
|
|
182
|
+
* }
|
|
183
|
+
* }
|
|
184
|
+
* };
|
|
185
|
+
* ```
|
|
186
|
+
*/
|
|
187
|
+
class InterceptorService {
|
|
188
|
+
constructor() {
|
|
189
|
+
this.interceptors = [];
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Register a new workflow interceptor that will wrap workflow execution.
|
|
193
|
+
* Interceptors are executed in the order they are registered, with the
|
|
194
|
+
* first registered interceptor being the outermost wrapper.
|
|
195
|
+
*
|
|
196
|
+
* @param interceptor The interceptor to register
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* ```typescript
|
|
200
|
+
* service.register({
|
|
201
|
+
* async execute(ctx, next) {
|
|
202
|
+
* console.time('workflow');
|
|
203
|
+
* try {
|
|
204
|
+
* return await next();
|
|
205
|
+
* } finally {
|
|
206
|
+
* console.timeEnd('workflow');
|
|
207
|
+
* }
|
|
208
|
+
* }
|
|
209
|
+
* });
|
|
210
|
+
* ```
|
|
211
|
+
*/
|
|
212
|
+
register(interceptor) {
|
|
213
|
+
this.interceptors.push(interceptor);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Execute the interceptor chain around the workflow function.
|
|
217
|
+
* The chain is built in an onion pattern where each interceptor
|
|
218
|
+
* wraps the next one, with the workflow function at the center.
|
|
219
|
+
*
|
|
220
|
+
* @param ctx The workflow context map
|
|
221
|
+
* @param fn The workflow function to execute
|
|
222
|
+
* @returns The result of the workflow execution
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* ```typescript
|
|
226
|
+
* // Execute workflow with timing
|
|
227
|
+
* const result = await service.executeChain(context, async () => {
|
|
228
|
+
* const start = Date.now();
|
|
229
|
+
* try {
|
|
230
|
+
* return await workflowFn();
|
|
231
|
+
* } finally {
|
|
232
|
+
* console.log('Duration:', Date.now() - start);
|
|
233
|
+
* }
|
|
234
|
+
* });
|
|
235
|
+
* ```
|
|
236
|
+
*/
|
|
237
|
+
async executeChain(ctx, fn) {
|
|
238
|
+
// Create the onion-like chain of interceptors
|
|
239
|
+
const chain = this.interceptors.reduceRight((next, interceptor) => {
|
|
240
|
+
return () => interceptor.execute(ctx, next);
|
|
241
|
+
}, fn);
|
|
242
|
+
return chain();
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Clear all registered interceptors.
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* ```typescript
|
|
249
|
+
* service.clear();
|
|
250
|
+
* ```
|
|
251
|
+
*/
|
|
252
|
+
clear() {
|
|
253
|
+
this.interceptors = [];
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
exports.InterceptorService = InterceptorService;
|
|
@@ -10,6 +10,7 @@ const hotmesh_1 = require("../hotmesh");
|
|
|
10
10
|
const stream_1 = require("../../types/stream");
|
|
11
11
|
const search_1 = require("./search");
|
|
12
12
|
const factory_1 = require("./schemas/factory");
|
|
13
|
+
const index_1 = require("./index");
|
|
13
14
|
/**
|
|
14
15
|
* The *Worker* service Registers worker functions and connects them to the mesh,
|
|
15
16
|
* using the target backend provider/s (Redis, Postgres, NATS, etc).
|
|
@@ -321,8 +322,16 @@ class WorkerService {
|
|
|
321
322
|
const [cursor, replay] = await store.findJobFields(workflowInput.workflowId, replayQuery, 50000, 5000);
|
|
322
323
|
context.set('replay', replay);
|
|
323
324
|
context.set('cursor', cursor); // if != 0, more remain
|
|
325
|
+
// Execute workflow with interceptors
|
|
324
326
|
const workflowResponse = await storage_1.asyncLocalStorage.run(context, async () => {
|
|
325
|
-
|
|
327
|
+
// Get the interceptor service
|
|
328
|
+
const interceptorService = index_1.MemFlow.getInterceptorService();
|
|
329
|
+
// Create the workflow execution function
|
|
330
|
+
const execWorkflow = async () => {
|
|
331
|
+
return await workflowFunction.apply(this, workflowInput.arguments);
|
|
332
|
+
};
|
|
333
|
+
// Execute the workflow through the interceptor chain
|
|
334
|
+
return await interceptorService.executeChain(context, execWorkflow);
|
|
326
335
|
});
|
|
327
336
|
//if the embedded function has a try/catch, it can interrup the throw
|
|
328
337
|
// throw here to interrupt the workflow if the embedded function caught and suppressed
|
|
@@ -22,7 +22,9 @@ function getChildInterruptPayload(context, options, execIndex) {
|
|
|
22
22
|
}
|
|
23
23
|
const parentWorkflowId = workflowId;
|
|
24
24
|
const taskQueueName = options.taskQueue ?? options.entity;
|
|
25
|
-
const workflowName = options.taskQueue
|
|
25
|
+
const workflowName = options.taskQueue
|
|
26
|
+
? options.workflowName
|
|
27
|
+
: options.entity ?? options.workflowName;
|
|
26
28
|
const workflowTopic = `${taskQueueName}-${workflowName}`;
|
|
27
29
|
return {
|
|
28
30
|
arguments: [...(options.args || [])],
|
|
@@ -67,7 +67,7 @@ async function execHook(options) {
|
|
|
67
67
|
}
|
|
68
68
|
const hookOptions = {
|
|
69
69
|
...options,
|
|
70
|
-
args: [...options.args, { signal: options.signalId, $memflow: true }]
|
|
70
|
+
args: [...options.args, { signal: options.signalId, $memflow: true }],
|
|
71
71
|
};
|
|
72
72
|
// Execute the hook with the signal information
|
|
73
73
|
await (0, hook_1.hook)(hookOptions);
|
|
@@ -27,7 +27,9 @@ async function hook(options) {
|
|
|
27
27
|
targetTopic = workflowTopic;
|
|
28
28
|
}
|
|
29
29
|
// DEFENSIVE CHECK: Prevent infinite loops
|
|
30
|
-
if (targetTopic === workflowTopic &&
|
|
30
|
+
if (targetTopic === workflowTopic &&
|
|
31
|
+
!options.entity &&
|
|
32
|
+
!options.taskQueue) {
|
|
31
33
|
throw new Error(`MemFlow Hook Error: Potential infinite loop detected!\n\n` +
|
|
32
34
|
`The hook would target the same workflow topic ('${workflowTopic}') as the current workflow, ` +
|
|
33
35
|
`creating an infinite loop.\n\n` +
|
|
@@ -39,7 +41,7 @@ async function hook(options) {
|
|
|
39
41
|
`Provided options: ${JSON.stringify({
|
|
40
42
|
workflowName: options.workflowName,
|
|
41
43
|
taskQueue: options.taskQueue,
|
|
42
|
-
entity: options.entity
|
|
44
|
+
entity: options.entity,
|
|
43
45
|
}, null, 2)}`);
|
|
44
46
|
}
|
|
45
47
|
const payload = {
|
|
@@ -55,7 +55,8 @@ function wrapActivity(activityName, options) {
|
|
|
55
55
|
throw new common_1.MemFlowTimeoutError(message, stack);
|
|
56
56
|
}
|
|
57
57
|
else {
|
|
58
|
-
//
|
|
58
|
+
// For any other error code, throw a MemFlowFatalError to stop the workflow
|
|
59
|
+
throw new common_1.MemFlowFatalError(message, stack);
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
return result.$error;
|
|
@@ -49,12 +49,20 @@ class ConsumptionManager {
|
|
|
49
49
|
const features = this.stream.getProviderSpecificFeatures();
|
|
50
50
|
const supportsNotifications = features.supportsNotifications;
|
|
51
51
|
if (supportsNotifications) {
|
|
52
|
-
this.logger.info(`router-stream-using-notifications`, {
|
|
52
|
+
this.logger.info(`router-stream-using-notifications`, {
|
|
53
|
+
group,
|
|
54
|
+
consumer,
|
|
55
|
+
stream,
|
|
56
|
+
});
|
|
53
57
|
this.lifecycleManager.setIsUsingNotifications(true);
|
|
54
58
|
return this.consumeWithNotifications(stream, group, consumer, callback);
|
|
55
59
|
}
|
|
56
60
|
else {
|
|
57
|
-
this.logger.info(`router-stream-using-polling`, {
|
|
61
|
+
this.logger.info(`router-stream-using-polling`, {
|
|
62
|
+
group,
|
|
63
|
+
consumer,
|
|
64
|
+
stream,
|
|
65
|
+
});
|
|
58
66
|
this.lifecycleManager.setIsUsingNotifications(false);
|
|
59
67
|
return this.consumeWithPolling(stream, group, consumer, callback);
|
|
60
68
|
}
|
|
@@ -67,7 +75,8 @@ class ConsumptionManager {
|
|
|
67
75
|
return;
|
|
68
76
|
}
|
|
69
77
|
await this.throttleManager.customSleep(); // respect throttle
|
|
70
|
-
if (this.lifecycleManager.isStopped(group, consumer, stream) ||
|
|
78
|
+
if (this.lifecycleManager.isStopped(group, consumer, stream) ||
|
|
79
|
+
this.throttleManager.isPaused()) {
|
|
71
80
|
return;
|
|
72
81
|
}
|
|
73
82
|
// Process messages - use parallel processing for PostgreSQL
|
|
@@ -78,7 +87,7 @@ class ConsumptionManager {
|
|
|
78
87
|
this.logger.debug('postgres-stream-parallel-processing', {
|
|
79
88
|
streamName: stream,
|
|
80
89
|
groupName: group,
|
|
81
|
-
messageCount: messages.length
|
|
90
|
+
messageCount: messages.length,
|
|
82
91
|
});
|
|
83
92
|
const processingStart = Date.now();
|
|
84
93
|
const processingPromises = messages.map(async (message) => {
|
|
@@ -93,7 +102,7 @@ class ConsumptionManager {
|
|
|
93
102
|
streamName: stream,
|
|
94
103
|
groupName: group,
|
|
95
104
|
messageCount: messages.length,
|
|
96
|
-
processingDuration: Date.now() - processingStart
|
|
105
|
+
processingDuration: Date.now() - processingStart,
|
|
97
106
|
});
|
|
98
107
|
}
|
|
99
108
|
else {
|
|
@@ -153,7 +162,11 @@ class ConsumptionManager {
|
|
|
153
162
|
consumer,
|
|
154
163
|
});
|
|
155
164
|
// Fall back to polling if notifications fail
|
|
156
|
-
this.logger.info(`router-stream-fallback-to-polling`, {
|
|
165
|
+
this.logger.info(`router-stream-fallback-to-polling`, {
|
|
166
|
+
group,
|
|
167
|
+
consumer,
|
|
168
|
+
stream,
|
|
169
|
+
});
|
|
157
170
|
this.lifecycleManager.setIsUsingNotifications(false);
|
|
158
171
|
return this.consumeWithPolling(stream, group, consumer, callback);
|
|
159
172
|
}
|
|
@@ -223,7 +236,7 @@ class ConsumptionManager {
|
|
|
223
236
|
this.logger.debug('postgres-stream-parallel-processing-polling', {
|
|
224
237
|
streamName: stream,
|
|
225
238
|
groupName: group,
|
|
226
|
-
messageCount: messages.length
|
|
239
|
+
messageCount: messages.length,
|
|
227
240
|
});
|
|
228
241
|
const processingStart = Date.now();
|
|
229
242
|
const processingPromises = messages.map(async (message) => {
|
|
@@ -238,7 +251,7 @@ class ConsumptionManager {
|
|
|
238
251
|
streamName: stream,
|
|
239
252
|
groupName: group,
|
|
240
253
|
messageCount: messages.length,
|
|
241
|
-
processingDuration: Date.now() - processingStart
|
|
254
|
+
processingDuration: Date.now() - processingStart,
|
|
242
255
|
});
|
|
243
256
|
}
|
|
244
257
|
else {
|
|
@@ -299,7 +312,8 @@ class ConsumptionManager {
|
|
|
299
312
|
}
|
|
300
313
|
}
|
|
301
314
|
catch (error) {
|
|
302
|
-
if (this.lifecycleManager.getShouldConsume() &&
|
|
315
|
+
if (this.lifecycleManager.getShouldConsume() &&
|
|
316
|
+
process.env.NODE_ENV !== 'test') {
|
|
303
317
|
this.logger.error(`router-stream-error`, {
|
|
304
318
|
error,
|
|
305
319
|
stream,
|
|
@@ -82,16 +82,16 @@ class ErrorHandler {
|
|
|
82
82
|
const [shouldRetry, timeout] = this.shouldRetry(input, output);
|
|
83
83
|
if (shouldRetry) {
|
|
84
84
|
await (0, utils_1.sleepFor)(timeout);
|
|
85
|
-
return await publishMessage(input.metadata.topic, {
|
|
85
|
+
return (await publishMessage(input.metadata.topic, {
|
|
86
86
|
data: input.data,
|
|
87
87
|
//note: retain guid (this is a retry attempt)
|
|
88
88
|
metadata: { ...input.metadata, try: (input.metadata.try || 0) + 1 },
|
|
89
89
|
policies: input.policies,
|
|
90
|
-
});
|
|
90
|
+
}));
|
|
91
91
|
}
|
|
92
92
|
else {
|
|
93
93
|
const structuredError = this.structureError(input, output);
|
|
94
|
-
return await publishMessage(null, structuredError);
|
|
94
|
+
return (await publishMessage(null, structuredError));
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
}
|