@hotmeshio/hotmesh 0.5.4 → 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 +185 -161
- 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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ContextType } from '../../types/memflow';
|
|
1
|
+
import { ContextType, WorkflowInterceptor } from '../../types/memflow';
|
|
2
2
|
import { ClientService } from './client';
|
|
3
3
|
import { ConnectionService } from './connection';
|
|
4
4
|
import { Search } from './search';
|
|
@@ -8,60 +8,170 @@ import { WorkflowService } from './workflow';
|
|
|
8
8
|
import { WorkflowHandleService } from './handle';
|
|
9
9
|
import { didInterrupt } from './workflow/interruption';
|
|
10
10
|
/**
|
|
11
|
-
* The MemFlow service
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* demonstrates how to start a new workflow, subscribe
|
|
15
|
-
* to the result, and shutdown the system.
|
|
11
|
+
* The MemFlow service provides a Temporal-compatible workflow framework backed by
|
|
12
|
+
* Postgres or Redis/ValKey. It offers durable execution, entity-based memory management,
|
|
13
|
+
* and composable workflows.
|
|
16
14
|
*
|
|
17
|
-
*
|
|
15
|
+
* ## Core Features
|
|
16
|
+
*
|
|
17
|
+
* ### 1. Entity-Based Memory Model
|
|
18
|
+
* Each workflow has a durable JSONB entity that serves as its memory:
|
|
19
|
+
* ```typescript
|
|
20
|
+
* export async function researchAgent(query: string) {
|
|
21
|
+
* const agent = await MemFlow.workflow.entity();
|
|
22
|
+
*
|
|
23
|
+
* // Initialize entity state
|
|
24
|
+
* await agent.set({
|
|
25
|
+
* query,
|
|
26
|
+
* findings: [],
|
|
27
|
+
* status: 'researching'
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* // Update state atomically
|
|
31
|
+
* await agent.merge({ status: 'analyzing' });
|
|
32
|
+
* await agent.append('findings', newFinding);
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* ### 2. Hook Functions & Workflow Coordination
|
|
37
|
+
* Spawn and coordinate multiple perspectives/phases:
|
|
18
38
|
* ```typescript
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
39
|
+
* // Launch parallel research perspectives
|
|
40
|
+
* await MemFlow.workflow.execHook({
|
|
41
|
+
* taskQueue: 'research',
|
|
42
|
+
* workflowName: 'optimisticView',
|
|
43
|
+
* args: [query],
|
|
44
|
+
* signalId: 'optimistic-complete'
|
|
45
|
+
* });
|
|
46
|
+
*
|
|
47
|
+
* await MemFlow.workflow.execHook({
|
|
48
|
+
* taskQueue: 'research',
|
|
49
|
+
* workflowName: 'skepticalView',
|
|
50
|
+
* args: [query],
|
|
51
|
+
* signalId: 'skeptical-complete'
|
|
52
|
+
* });
|
|
22
53
|
*
|
|
23
|
-
* //
|
|
54
|
+
* // Wait for both perspectives
|
|
55
|
+
* await Promise.all([
|
|
56
|
+
* MemFlow.workflow.waitFor('optimistic-complete'),
|
|
57
|
+
* MemFlow.workflow.waitFor('skeptical-complete')
|
|
58
|
+
* ]);
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
* ### 3. Durable Activities & Proxies
|
|
62
|
+
* Define and execute durable activities with automatic retry:
|
|
63
|
+
* ```typescript
|
|
64
|
+
* const activities = MemFlow.workflow.proxyActivities<{
|
|
65
|
+
* analyzeDocument: typeof analyzeDocument;
|
|
66
|
+
* validateFindings: typeof validateFindings;
|
|
67
|
+
* }>({
|
|
68
|
+
* activities: { analyzeDocument, validateFindings },
|
|
69
|
+
* retryPolicy: {
|
|
70
|
+
* maximumAttempts: 3,
|
|
71
|
+
* backoffCoefficient: 2
|
|
72
|
+
* }
|
|
73
|
+
* });
|
|
74
|
+
*
|
|
75
|
+
* // Activities are durable and automatically retried
|
|
76
|
+
* const analysis = await activities.analyzeDocument(data);
|
|
77
|
+
* const validation = await activities.validateFindings(analysis);
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* ### 4. Workflow Composition
|
|
81
|
+
* Build complex workflows through composition:
|
|
82
|
+
* ```typescript
|
|
83
|
+
* // Start a child workflow
|
|
84
|
+
* const childResult = await MemFlow.workflow.execChild({
|
|
85
|
+
* taskQueue: 'analysis',
|
|
86
|
+
* workflowName: 'detailedAnalysis',
|
|
87
|
+
* args: [data],
|
|
88
|
+
* // Child workflow config
|
|
89
|
+
* config: {
|
|
90
|
+
* maximumAttempts: 5,
|
|
91
|
+
* backoffCoefficient: 2
|
|
92
|
+
* }
|
|
93
|
+
* });
|
|
94
|
+
*
|
|
95
|
+
* // Fire-and-forget child workflow
|
|
96
|
+
* await MemFlow.workflow.startChild({
|
|
97
|
+
* taskQueue: 'notifications',
|
|
98
|
+
* workflowName: 'sendUpdates',
|
|
99
|
+
* args: [updates]
|
|
100
|
+
* });
|
|
101
|
+
* ```
|
|
102
|
+
*
|
|
103
|
+
* ### 5. Workflow Interceptors
|
|
104
|
+
* Add cross-cutting concerns through interceptors that run as durable functions:
|
|
105
|
+
* ```typescript
|
|
106
|
+
* // Add audit interceptor that uses MemFlow functions
|
|
107
|
+
* MemFlow.registerInterceptor({
|
|
108
|
+
* async execute(ctx, next) {
|
|
109
|
+
* try {
|
|
110
|
+
* // Interceptors can use MemFlow functions and participate in replay
|
|
111
|
+
* const entity = await MemFlow.workflow.entity();
|
|
112
|
+
* await entity.append('auditLog', {
|
|
113
|
+
* action: 'started',
|
|
114
|
+
* timestamp: new Date().toISOString()
|
|
115
|
+
* });
|
|
116
|
+
*
|
|
117
|
+
* // Rate limiting with durable sleep
|
|
118
|
+
* await MemFlow.workflow.sleepFor('100 milliseconds');
|
|
119
|
+
*
|
|
120
|
+
* const result = await next();
|
|
121
|
+
*
|
|
122
|
+
* await entity.append('auditLog', {
|
|
123
|
+
* action: 'completed',
|
|
124
|
+
* timestamp: new Date().toISOString()
|
|
125
|
+
* });
|
|
126
|
+
*
|
|
127
|
+
* return result;
|
|
128
|
+
* } catch (err) {
|
|
129
|
+
* // CRITICAL: Always check for HotMesh interruptions
|
|
130
|
+
* if (MemFlow.didInterrupt(err)) {
|
|
131
|
+
* throw err; // Rethrow for replay system
|
|
132
|
+
* }
|
|
133
|
+
* throw err;
|
|
134
|
+
* }
|
|
135
|
+
* }
|
|
136
|
+
* });
|
|
137
|
+
* ```
|
|
138
|
+
*
|
|
139
|
+
* ## Basic Usage Example
|
|
140
|
+
*
|
|
141
|
+
* ```typescript
|
|
142
|
+
* import { Client, Worker, MemFlow } from '@hotmeshio/hotmesh';
|
|
143
|
+
* import { Client as Postgres } from 'pg';
|
|
144
|
+
*
|
|
145
|
+
* // Initialize worker
|
|
24
146
|
* await Worker.create({
|
|
25
147
|
* connection: {
|
|
26
148
|
* class: Postgres,
|
|
27
|
-
* options: {
|
|
28
|
-
*
|
|
29
|
-
* }
|
|
30
|
-
* }
|
|
149
|
+
* options: { connectionString: 'postgresql://usr:pwd@localhost:5432/db' }
|
|
150
|
+
* },
|
|
31
151
|
* taskQueue: 'default',
|
|
32
|
-
*
|
|
33
|
-
* workflow: workflows.example,
|
|
34
|
-
* options: {
|
|
35
|
-
* backoffCoefficient: 2,
|
|
36
|
-
* maximumAttempts: 1_000,
|
|
37
|
-
* maximumInterval: '5 seconds'
|
|
38
|
-
* }
|
|
152
|
+
* workflow: workflows.example
|
|
39
153
|
* });
|
|
40
154
|
*
|
|
41
|
-
* //
|
|
155
|
+
* // Initialize client
|
|
42
156
|
* const client = new Client({
|
|
43
157
|
* connection: {
|
|
44
158
|
* class: Postgres,
|
|
45
|
-
* options: {
|
|
46
|
-
* connectionString: 'postgresql://usr:pwd@localhost:5432/db',
|
|
47
|
-
* }
|
|
159
|
+
* options: { connectionString: 'postgresql://usr:pwd@localhost:5432/db' }
|
|
48
160
|
* }
|
|
49
161
|
* });
|
|
50
162
|
*
|
|
51
|
-
* //
|
|
163
|
+
* // Start workflow
|
|
52
164
|
* const handle = await client.workflow.start({
|
|
53
|
-
* args: ['
|
|
165
|
+
* args: ['input data'],
|
|
54
166
|
* taskQueue: 'default',
|
|
55
167
|
* workflowName: 'example',
|
|
56
|
-
* workflowId:
|
|
57
|
-
* namespace: 'memflow',
|
|
168
|
+
* workflowId: MemFlow.guid()
|
|
58
169
|
* });
|
|
59
170
|
*
|
|
60
|
-
* //
|
|
61
|
-
*
|
|
62
|
-
* //logs '¡Hola, HotMesh!'
|
|
171
|
+
* // Get result
|
|
172
|
+
* const result = await handle.result();
|
|
63
173
|
*
|
|
64
|
-
* //
|
|
174
|
+
* // Cleanup
|
|
65
175
|
* await MemFlow.shutdown();
|
|
66
176
|
* ```
|
|
67
177
|
*/
|
|
@@ -114,6 +224,16 @@ declare class MemFlowClass {
|
|
|
114
224
|
* @see {@link utils/interruption.didInterrupt} for detailed documentation
|
|
115
225
|
*/
|
|
116
226
|
static didInterrupt: typeof didInterrupt;
|
|
227
|
+
private static interceptorService;
|
|
228
|
+
/**
|
|
229
|
+
* Register a workflow interceptor
|
|
230
|
+
* @param interceptor The interceptor to register
|
|
231
|
+
*/
|
|
232
|
+
static registerInterceptor(interceptor: WorkflowInterceptor): void;
|
|
233
|
+
/**
|
|
234
|
+
* Clear all registered workflow interceptors
|
|
235
|
+
*/
|
|
236
|
+
static clearInterceptors(): void;
|
|
117
237
|
/**
|
|
118
238
|
* Shutdown everything. All connections, workers, and clients will be closed.
|
|
119
239
|
* Include in your signal handlers to ensure a clean shutdown.
|
|
@@ -10,61 +10,172 @@ const worker_1 = require("./worker");
|
|
|
10
10
|
const workflow_1 = require("./workflow");
|
|
11
11
|
const handle_1 = require("./handle");
|
|
12
12
|
const interruption_1 = require("./workflow/interruption");
|
|
13
|
+
const interceptor_1 = require("./interceptor");
|
|
13
14
|
/**
|
|
14
|
-
* The MemFlow service
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* demonstrates how to start a new workflow, subscribe
|
|
18
|
-
* to the result, and shutdown the system.
|
|
15
|
+
* The MemFlow service provides a Temporal-compatible workflow framework backed by
|
|
16
|
+
* Postgres or Redis/ValKey. It offers durable execution, entity-based memory management,
|
|
17
|
+
* and composable workflows.
|
|
19
18
|
*
|
|
20
|
-
*
|
|
19
|
+
* ## Core Features
|
|
20
|
+
*
|
|
21
|
+
* ### 1. Entity-Based Memory Model
|
|
22
|
+
* Each workflow has a durable JSONB entity that serves as its memory:
|
|
23
|
+
* ```typescript
|
|
24
|
+
* export async function researchAgent(query: string) {
|
|
25
|
+
* const agent = await MemFlow.workflow.entity();
|
|
26
|
+
*
|
|
27
|
+
* // Initialize entity state
|
|
28
|
+
* await agent.set({
|
|
29
|
+
* query,
|
|
30
|
+
* findings: [],
|
|
31
|
+
* status: 'researching'
|
|
32
|
+
* });
|
|
33
|
+
*
|
|
34
|
+
* // Update state atomically
|
|
35
|
+
* await agent.merge({ status: 'analyzing' });
|
|
36
|
+
* await agent.append('findings', newFinding);
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* ### 2. Hook Functions & Workflow Coordination
|
|
41
|
+
* Spawn and coordinate multiple perspectives/phases:
|
|
42
|
+
* ```typescript
|
|
43
|
+
* // Launch parallel research perspectives
|
|
44
|
+
* await MemFlow.workflow.execHook({
|
|
45
|
+
* taskQueue: 'research',
|
|
46
|
+
* workflowName: 'optimisticView',
|
|
47
|
+
* args: [query],
|
|
48
|
+
* signalId: 'optimistic-complete'
|
|
49
|
+
* });
|
|
50
|
+
*
|
|
51
|
+
* await MemFlow.workflow.execHook({
|
|
52
|
+
* taskQueue: 'research',
|
|
53
|
+
* workflowName: 'skepticalView',
|
|
54
|
+
* args: [query],
|
|
55
|
+
* signalId: 'skeptical-complete'
|
|
56
|
+
* });
|
|
57
|
+
*
|
|
58
|
+
* // Wait for both perspectives
|
|
59
|
+
* await Promise.all([
|
|
60
|
+
* MemFlow.workflow.waitFor('optimistic-complete'),
|
|
61
|
+
* MemFlow.workflow.waitFor('skeptical-complete')
|
|
62
|
+
* ]);
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* ### 3. Durable Activities & Proxies
|
|
66
|
+
* Define and execute durable activities with automatic retry:
|
|
67
|
+
* ```typescript
|
|
68
|
+
* const activities = MemFlow.workflow.proxyActivities<{
|
|
69
|
+
* analyzeDocument: typeof analyzeDocument;
|
|
70
|
+
* validateFindings: typeof validateFindings;
|
|
71
|
+
* }>({
|
|
72
|
+
* activities: { analyzeDocument, validateFindings },
|
|
73
|
+
* retryPolicy: {
|
|
74
|
+
* maximumAttempts: 3,
|
|
75
|
+
* backoffCoefficient: 2
|
|
76
|
+
* }
|
|
77
|
+
* });
|
|
78
|
+
*
|
|
79
|
+
* // Activities are durable and automatically retried
|
|
80
|
+
* const analysis = await activities.analyzeDocument(data);
|
|
81
|
+
* const validation = await activities.validateFindings(analysis);
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* ### 4. Workflow Composition
|
|
85
|
+
* Build complex workflows through composition:
|
|
86
|
+
* ```typescript
|
|
87
|
+
* // Start a child workflow
|
|
88
|
+
* const childResult = await MemFlow.workflow.execChild({
|
|
89
|
+
* taskQueue: 'analysis',
|
|
90
|
+
* workflowName: 'detailedAnalysis',
|
|
91
|
+
* args: [data],
|
|
92
|
+
* // Child workflow config
|
|
93
|
+
* config: {
|
|
94
|
+
* maximumAttempts: 5,
|
|
95
|
+
* backoffCoefficient: 2
|
|
96
|
+
* }
|
|
97
|
+
* });
|
|
98
|
+
*
|
|
99
|
+
* // Fire-and-forget child workflow
|
|
100
|
+
* await MemFlow.workflow.startChild({
|
|
101
|
+
* taskQueue: 'notifications',
|
|
102
|
+
* workflowName: 'sendUpdates',
|
|
103
|
+
* args: [updates]
|
|
104
|
+
* });
|
|
105
|
+
* ```
|
|
106
|
+
*
|
|
107
|
+
* ### 5. Workflow Interceptors
|
|
108
|
+
* Add cross-cutting concerns through interceptors that run as durable functions:
|
|
21
109
|
* ```typescript
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
110
|
+
* // Add audit interceptor that uses MemFlow functions
|
|
111
|
+
* MemFlow.registerInterceptor({
|
|
112
|
+
* async execute(ctx, next) {
|
|
113
|
+
* try {
|
|
114
|
+
* // Interceptors can use MemFlow functions and participate in replay
|
|
115
|
+
* const entity = await MemFlow.workflow.entity();
|
|
116
|
+
* await entity.append('auditLog', {
|
|
117
|
+
* action: 'started',
|
|
118
|
+
* timestamp: new Date().toISOString()
|
|
119
|
+
* });
|
|
120
|
+
*
|
|
121
|
+
* // Rate limiting with durable sleep
|
|
122
|
+
* await MemFlow.workflow.sleepFor('100 milliseconds');
|
|
123
|
+
*
|
|
124
|
+
* const result = await next();
|
|
125
|
+
*
|
|
126
|
+
* await entity.append('auditLog', {
|
|
127
|
+
* action: 'completed',
|
|
128
|
+
* timestamp: new Date().toISOString()
|
|
129
|
+
* });
|
|
25
130
|
*
|
|
26
|
-
*
|
|
131
|
+
* return result;
|
|
132
|
+
* } catch (err) {
|
|
133
|
+
* // CRITICAL: Always check for HotMesh interruptions
|
|
134
|
+
* if (MemFlow.didInterrupt(err)) {
|
|
135
|
+
* throw err; // Rethrow for replay system
|
|
136
|
+
* }
|
|
137
|
+
* throw err;
|
|
138
|
+
* }
|
|
139
|
+
* }
|
|
140
|
+
* });
|
|
141
|
+
* ```
|
|
142
|
+
*
|
|
143
|
+
* ## Basic Usage Example
|
|
144
|
+
*
|
|
145
|
+
* ```typescript
|
|
146
|
+
* import { Client, Worker, MemFlow } from '@hotmeshio/hotmesh';
|
|
147
|
+
* import { Client as Postgres } from 'pg';
|
|
148
|
+
*
|
|
149
|
+
* // Initialize worker
|
|
27
150
|
* await Worker.create({
|
|
28
151
|
* connection: {
|
|
29
152
|
* class: Postgres,
|
|
30
|
-
* options: {
|
|
31
|
-
*
|
|
32
|
-
* }
|
|
33
|
-
* }
|
|
153
|
+
* options: { connectionString: 'postgresql://usr:pwd@localhost:5432/db' }
|
|
154
|
+
* },
|
|
34
155
|
* taskQueue: 'default',
|
|
35
|
-
*
|
|
36
|
-
* workflow: workflows.example,
|
|
37
|
-
* options: {
|
|
38
|
-
* backoffCoefficient: 2,
|
|
39
|
-
* maximumAttempts: 1_000,
|
|
40
|
-
* maximumInterval: '5 seconds'
|
|
41
|
-
* }
|
|
156
|
+
* workflow: workflows.example
|
|
42
157
|
* });
|
|
43
158
|
*
|
|
44
|
-
* //
|
|
159
|
+
* // Initialize client
|
|
45
160
|
* const client = new Client({
|
|
46
161
|
* connection: {
|
|
47
162
|
* class: Postgres,
|
|
48
|
-
* options: {
|
|
49
|
-
* connectionString: 'postgresql://usr:pwd@localhost:5432/db',
|
|
50
|
-
* }
|
|
163
|
+
* options: { connectionString: 'postgresql://usr:pwd@localhost:5432/db' }
|
|
51
164
|
* }
|
|
52
165
|
* });
|
|
53
166
|
*
|
|
54
|
-
* //
|
|
167
|
+
* // Start workflow
|
|
55
168
|
* const handle = await client.workflow.start({
|
|
56
|
-
* args: ['
|
|
169
|
+
* args: ['input data'],
|
|
57
170
|
* taskQueue: 'default',
|
|
58
171
|
* workflowName: 'example',
|
|
59
|
-
* workflowId:
|
|
60
|
-
* namespace: 'memflow',
|
|
172
|
+
* workflowId: MemFlow.guid()
|
|
61
173
|
* });
|
|
62
174
|
*
|
|
63
|
-
* //
|
|
64
|
-
*
|
|
65
|
-
* //logs '¡Hola, HotMesh!'
|
|
175
|
+
* // Get result
|
|
176
|
+
* const result = await handle.result();
|
|
66
177
|
*
|
|
67
|
-
* //
|
|
178
|
+
* // Cleanup
|
|
68
179
|
* await MemFlow.shutdown();
|
|
69
180
|
* ```
|
|
70
181
|
*/
|
|
@@ -73,6 +184,26 @@ class MemFlowClass {
|
|
|
73
184
|
* @private
|
|
74
185
|
*/
|
|
75
186
|
constructor() { }
|
|
187
|
+
/**
|
|
188
|
+
* Register a workflow interceptor
|
|
189
|
+
* @param interceptor The interceptor to register
|
|
190
|
+
*/
|
|
191
|
+
static registerInterceptor(interceptor) {
|
|
192
|
+
MemFlowClass.interceptorService.register(interceptor);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Clear all registered workflow interceptors
|
|
196
|
+
*/
|
|
197
|
+
static clearInterceptors() {
|
|
198
|
+
MemFlowClass.interceptorService.clear();
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Get the interceptor service instance
|
|
202
|
+
* @internal
|
|
203
|
+
*/
|
|
204
|
+
static getInterceptorService() {
|
|
205
|
+
return MemFlowClass.interceptorService;
|
|
206
|
+
}
|
|
76
207
|
/**
|
|
77
208
|
* Shutdown everything. All connections, workers, and clients will be closed.
|
|
78
209
|
* Include in your signal handlers to ensure a clean shutdown.
|
|
@@ -128,3 +259,4 @@ MemFlowClass.workflow = workflow_1.WorkflowService;
|
|
|
128
259
|
* @see {@link utils/interruption.didInterrupt} for detailed documentation
|
|
129
260
|
*/
|
|
130
261
|
MemFlowClass.didInterrupt = interruption_1.didInterrupt;
|
|
262
|
+
MemFlowClass.interceptorService = new interceptor_1.InterceptorService();
|
|
@@ -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
|
+
}
|