@hotmeshio/hotmesh 0.7.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 +8 -0
- package/README.md +158 -38
- package/build/index.d.ts +1 -3
- package/build/index.js +1 -5
- package/build/modules/utils.js +3 -31
- package/build/package.json +63 -79
- package/build/services/activities/activity.d.ts +97 -9
- package/build/services/activities/activity.js +323 -86
- package/build/services/activities/await.d.ts +101 -0
- package/build/services/activities/await.js +103 -2
- package/build/services/activities/cycle.d.ts +82 -0
- package/build/services/activities/cycle.js +86 -8
- package/build/services/activities/hook.d.ts +144 -1
- package/build/services/activities/hook.js +162 -21
- package/build/services/activities/interrupt.d.ts +112 -0
- package/build/services/activities/interrupt.js +134 -29
- package/build/services/activities/signal.d.ts +111 -4
- package/build/services/activities/signal.js +136 -28
- 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 +109 -2
- package/build/services/collator/index.d.ts +116 -30
- package/build/services/collator/index.js +211 -115
- package/build/services/connector/factory.d.ts +1 -1
- package/build/services/connector/factory.js +1 -11
- package/build/services/engine/index.d.ts +22 -6
- package/build/services/engine/index.js +49 -18
- 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/router/consumption/index.js +1 -1
- package/build/services/search/factory.js +1 -9
- package/build/services/store/factory.js +1 -9
- package/build/services/store/index.d.ts +6 -1
- package/build/services/store/providers/postgres/kvsql.d.ts +4 -0
- package/build/services/store/providers/postgres/kvsql.js +4 -0
- package/build/services/store/providers/postgres/kvtransaction.d.ts +2 -0
- package/build/services/store/providers/postgres/kvtransaction.js +23 -0
- package/build/services/store/providers/postgres/kvtypes/hash/basic.d.ts +51 -0
- package/build/services/store/providers/postgres/kvtypes/hash/basic.js +193 -1
- package/build/services/store/providers/postgres/kvtypes/hash/index.d.ts +4 -0
- package/build/services/store/providers/postgres/kvtypes/hash/index.js +6 -0
- package/build/services/store/providers/postgres/postgres.d.ts +21 -1
- package/build/services/store/providers/postgres/postgres.js +42 -4
- package/build/services/stream/factory.js +1 -17
- package/build/services/stream/providers/postgres/scout.js +2 -2
- package/build/services/sub/factory.js +1 -9
- package/build/services/sub/index.d.ts +1 -1
- package/build/services/sub/providers/postgres/postgres.d.ts +1 -1
- package/build/services/sub/providers/postgres/postgres.js +25 -10
- package/build/services/task/index.d.ts +1 -1
- package/build/services/task/index.js +2 -6
- 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/index.d.ts +0 -1
- package/build/types/index.js +1 -4
- package/build/types/job.d.ts +1 -1
- package/build/types/memflow.d.ts +1 -1
- package/build/types/provider.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/index.ts +0 -4
- package/package.json +63 -79
- package/vitest.config.ts +17 -0
- package/build/services/connector/providers/ioredis.d.ts +0 -9
- package/build/services/connector/providers/ioredis.js +0 -26
- package/build/services/connector/providers/redis.d.ts +0 -9
- package/build/services/connector/providers/redis.js +0 -38
- package/build/services/search/providers/redis/ioredis.d.ts +0 -23
- package/build/services/search/providers/redis/ioredis.js +0 -189
- package/build/services/search/providers/redis/redis.d.ts +0 -23
- package/build/services/search/providers/redis/redis.js +0 -202
- package/build/services/store/providers/redis/_base.d.ts +0 -137
- package/build/services/store/providers/redis/_base.js +0 -980
- package/build/services/store/providers/redis/ioredis.d.ts +0 -20
- package/build/services/store/providers/redis/ioredis.js +0 -190
- package/build/services/store/providers/redis/redis.d.ts +0 -18
- package/build/services/store/providers/redis/redis.js +0 -199
- package/build/services/stream/providers/redis/ioredis.d.ts +0 -61
- package/build/services/stream/providers/redis/ioredis.js +0 -272
- package/build/services/stream/providers/redis/redis.d.ts +0 -61
- package/build/services/stream/providers/redis/redis.js +0 -305
- package/build/services/sub/providers/redis/ioredis.d.ts +0 -20
- package/build/services/sub/providers/redis/ioredis.js +0 -161
- package/build/services/sub/providers/redis/redis.d.ts +0 -18
- package/build/services/sub/providers/redis/redis.js +0 -148
- package/build/types/redis.d.ts +0 -258
- package/build/types/redis.js +0 -11
|
@@ -9,7 +9,145 @@ const telemetry_1 = require("../telemetry");
|
|
|
9
9
|
const stream_1 = require("../../types/stream");
|
|
10
10
|
const activity_1 = require("./activity");
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
12
|
+
* A versatile pause/resume activity that supports three distinct patterns:
|
|
13
|
+
* **time hook** (sleep), **web hook** (external signal), and **passthrough**
|
|
14
|
+
* (immediate transition with optional data mapping).
|
|
15
|
+
*
|
|
16
|
+
* The hook activity is the most flexible activity type. Depending on its
|
|
17
|
+
* YAML configuration, it operates in one of the following modes:
|
|
18
|
+
*
|
|
19
|
+
* ## Time Hook (Sleep)
|
|
20
|
+
*
|
|
21
|
+
* Pauses the flow for a specified duration in seconds. The `sleep` value
|
|
22
|
+
* can be a literal number or a `@pipe` expression for dynamic delays
|
|
23
|
+
* (e.g., exponential backoff).
|
|
24
|
+
*
|
|
25
|
+
* ```yaml
|
|
26
|
+
* app:
|
|
27
|
+
* id: myapp
|
|
28
|
+
* version: '1'
|
|
29
|
+
* graphs:
|
|
30
|
+
* - subscribes: job.start
|
|
31
|
+
* expire: 300
|
|
32
|
+
*
|
|
33
|
+
* activities:
|
|
34
|
+
* t1:
|
|
35
|
+
* type: trigger
|
|
36
|
+
*
|
|
37
|
+
* delay:
|
|
38
|
+
* type: hook
|
|
39
|
+
* sleep: 60 # pause for 60 seconds
|
|
40
|
+
* job:
|
|
41
|
+
* maps:
|
|
42
|
+
* paused_at: '{$self.output.metadata.ac}'
|
|
43
|
+
*
|
|
44
|
+
* resume:
|
|
45
|
+
* type: hook
|
|
46
|
+
*
|
|
47
|
+
* transitions:
|
|
48
|
+
* t1:
|
|
49
|
+
* - to: delay
|
|
50
|
+
* delay:
|
|
51
|
+
* - to: resume
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* ## Web Hook (External Signal)
|
|
55
|
+
*
|
|
56
|
+
* Registers a webhook listener on a named topic. The flow pauses until
|
|
57
|
+
* an external signal is sent to the hook's topic. The signal data becomes
|
|
58
|
+
* available as `$self.hook.data`. The `hooks` section at the graph level
|
|
59
|
+
* routes incoming signals to the waiting activity.
|
|
60
|
+
*
|
|
61
|
+
* ```yaml
|
|
62
|
+
* app:
|
|
63
|
+
* id: myapp
|
|
64
|
+
* version: '1'
|
|
65
|
+
* graphs:
|
|
66
|
+
* - subscribes: order.placed
|
|
67
|
+
* expire: 3600
|
|
68
|
+
*
|
|
69
|
+
* activities:
|
|
70
|
+
* t1:
|
|
71
|
+
* type: trigger
|
|
72
|
+
*
|
|
73
|
+
* wait_for_approval:
|
|
74
|
+
* type: hook
|
|
75
|
+
* hook:
|
|
76
|
+
* type: object
|
|
77
|
+
* properties:
|
|
78
|
+
* approved: { type: boolean }
|
|
79
|
+
* job:
|
|
80
|
+
* maps:
|
|
81
|
+
* approved: '{$self.hook.data.approved}'
|
|
82
|
+
*
|
|
83
|
+
* done:
|
|
84
|
+
* type: hook
|
|
85
|
+
*
|
|
86
|
+
* transitions:
|
|
87
|
+
* t1:
|
|
88
|
+
* - to: wait_for_approval
|
|
89
|
+
* wait_for_approval:
|
|
90
|
+
* - to: done
|
|
91
|
+
*
|
|
92
|
+
* hooks:
|
|
93
|
+
* order.approval: # external topic that delivers the signal
|
|
94
|
+
* - to: wait_for_approval
|
|
95
|
+
* conditions:
|
|
96
|
+
* match:
|
|
97
|
+
* - expected: '{t1.output.data.id}'
|
|
98
|
+
* actual: '{$self.hook.data.id}'
|
|
99
|
+
* ```
|
|
100
|
+
*
|
|
101
|
+
* ## Passthrough (No Hook)
|
|
102
|
+
*
|
|
103
|
+
* When neither `sleep` nor `hook` is configured, the hook activity acts
|
|
104
|
+
* as a passthrough: it maps data and immediately transitions to children.
|
|
105
|
+
* This is useful for data transformation, convergence points, or as a
|
|
106
|
+
* cycle pivot (with `cycle: true`).
|
|
107
|
+
*
|
|
108
|
+
* ```yaml
|
|
109
|
+
* app:
|
|
110
|
+
* id: myapp
|
|
111
|
+
* version: '1'
|
|
112
|
+
* graphs:
|
|
113
|
+
* - subscribes: job.start
|
|
114
|
+
*
|
|
115
|
+
* activities:
|
|
116
|
+
* t1:
|
|
117
|
+
* type: trigger
|
|
118
|
+
*
|
|
119
|
+
* pivot:
|
|
120
|
+
* type: hook
|
|
121
|
+
* cycle: true # enables re-entry from a cycle activity
|
|
122
|
+
* output:
|
|
123
|
+
* maps:
|
|
124
|
+
* retryCount: 0
|
|
125
|
+
* job:
|
|
126
|
+
* maps:
|
|
127
|
+
* counter: '{$self.output.data.retryCount}'
|
|
128
|
+
*
|
|
129
|
+
* do_work:
|
|
130
|
+
* type: worker
|
|
131
|
+
* topic: work.do
|
|
132
|
+
*
|
|
133
|
+
* transitions:
|
|
134
|
+
* t1:
|
|
135
|
+
* - to: pivot
|
|
136
|
+
* pivot:
|
|
137
|
+
* - to: do_work
|
|
138
|
+
* ```
|
|
139
|
+
*
|
|
140
|
+
* ## Execution Model
|
|
141
|
+
*
|
|
142
|
+
* - **With `sleep` or `hook`**: Category A (duplex). Leg 1 registers the
|
|
143
|
+
* hook and saves state. Leg 2 fires when the timer expires or the
|
|
144
|
+
* external signal arrives (via `processTimeHookEvent` or
|
|
145
|
+
* `processWebHookEvent`).
|
|
146
|
+
* - **Without `sleep` or `hook`**: Category B (passthrough). Uses the
|
|
147
|
+
* crash-safe `executeLeg1StepProtocol` to map data and transition
|
|
148
|
+
* to adjacent activities.
|
|
149
|
+
*
|
|
150
|
+
* @see {@link HookActivity} for the TypeScript interface
|
|
13
151
|
*/
|
|
14
152
|
class Hook extends activity_1.Activity {
|
|
15
153
|
constructor(config, data, metadata, hook, engine, context) {
|
|
@@ -24,15 +162,21 @@ class Hook extends activity_1.Activity {
|
|
|
24
162
|
});
|
|
25
163
|
let telemetry;
|
|
26
164
|
try {
|
|
27
|
-
|
|
165
|
+
//Phase 1: Load state and verify entry (all paths use verifyLeg1Entry
|
|
166
|
+
//for GUID-ledger-backed crash recovery)
|
|
167
|
+
const isResume = await this.verifyLeg1Entry();
|
|
28
168
|
telemetry = new telemetry_1.TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
|
|
29
169
|
telemetry.startActivitySpan(this.leg);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
170
|
+
//Phase 2: Route based on RUNTIME evaluation (not static config)
|
|
171
|
+
if (this.isConfiguredAsHook() && this.doesHook()) {
|
|
172
|
+
//Category A: duplexed hook registration (Leg2 handles completion)
|
|
173
|
+
if (!isResume) {
|
|
174
|
+
await this.doHook(telemetry);
|
|
175
|
+
}
|
|
176
|
+
//If resume, Leg1 already ran — Leg2 will handle completion
|
|
33
177
|
}
|
|
34
178
|
else {
|
|
35
|
-
//
|
|
179
|
+
//Category B: passthrough with crash-safe step protocol + GUID ledger
|
|
36
180
|
await this.doPassThrough(telemetry);
|
|
37
181
|
}
|
|
38
182
|
return this.context.metadata.aid;
|
|
@@ -76,6 +220,13 @@ class Hook extends activity_1.Activity {
|
|
|
76
220
|
});
|
|
77
221
|
}
|
|
78
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Static config check: does this activity have a hook or sleep config?
|
|
225
|
+
* Used for routing before context is loaded.
|
|
226
|
+
*/
|
|
227
|
+
isConfiguredAsHook() {
|
|
228
|
+
return !!this.config.sleep || !!this.config.hook?.topic;
|
|
229
|
+
}
|
|
79
230
|
/**
|
|
80
231
|
* does this activity use a time-hook or web-hook
|
|
81
232
|
*/
|
|
@@ -92,29 +243,19 @@ class Hook extends activity_1.Activity {
|
|
|
92
243
|
this.mapOutputData();
|
|
93
244
|
this.mapJobData();
|
|
94
245
|
await this.setState(transaction);
|
|
95
|
-
await collator_1.CollatorService.
|
|
246
|
+
await collator_1.CollatorService.notarizeLeg1Completion(this, transaction);
|
|
96
247
|
await this.setStatus(0, transaction);
|
|
97
248
|
await transaction.exec();
|
|
98
249
|
telemetry.mapActivityAttributes();
|
|
99
250
|
}
|
|
100
251
|
async doPassThrough(telemetry) {
|
|
101
|
-
const transaction = this.store.transact();
|
|
102
|
-
let multiResponse;
|
|
103
252
|
this.adjacencyList = await this.filterAdjacent();
|
|
104
253
|
this.mapOutputData();
|
|
105
254
|
this.mapJobData();
|
|
106
|
-
|
|
107
|
-
await
|
|
108
|
-
await this.setStatus(this.adjacencyList.length - 1, transaction);
|
|
109
|
-
multiResponse = (await transaction.exec());
|
|
255
|
+
//Category B: use Leg1 step protocol for crash-safe edge capture
|
|
256
|
+
await this.executeLeg1StepProtocol(this.adjacencyList.length - 1);
|
|
110
257
|
telemetry.mapActivityAttributes();
|
|
111
|
-
|
|
112
|
-
const attrs = { 'app.job.jss': jobStatus };
|
|
113
|
-
const messageIds = await this.transition(this.adjacencyList, jobStatus);
|
|
114
|
-
if (messageIds.length) {
|
|
115
|
-
attrs['app.activity.mids'] = messageIds.join(',');
|
|
116
|
-
}
|
|
117
|
-
telemetry.setActivityAttributes(attrs);
|
|
258
|
+
telemetry.setActivityAttributes({});
|
|
118
259
|
}
|
|
119
260
|
async getHookRule(topic) {
|
|
120
261
|
const rules = await this.store.getHookRules();
|
|
@@ -126,7 +267,7 @@ class Hook extends activity_1.Activity {
|
|
|
126
267
|
}
|
|
127
268
|
else if (this.config.sleep) {
|
|
128
269
|
const duration = pipe_1.Pipe.resolve(this.config.sleep, this.context);
|
|
129
|
-
await this.engine.taskService.registerTimeHook(this.context.metadata.jid, this.context.metadata.gid, `${this.metadata.aid}${this.metadata.dad || ''}`, 'sleep', duration, this.metadata.dad || '');
|
|
270
|
+
await this.engine.taskService.registerTimeHook(this.context.metadata.jid, this.context.metadata.gid, `${this.metadata.aid}${this.metadata.dad || ''}`, 'sleep', duration, this.metadata.dad || '', transaction);
|
|
130
271
|
return this.context.metadata.jid;
|
|
131
272
|
}
|
|
132
273
|
}
|
|
@@ -3,6 +3,118 @@ import { TelemetryService } from '../telemetry';
|
|
|
3
3
|
import { ActivityData, ActivityMetadata, ActivityType, InterruptActivity } from '../../types/activity';
|
|
4
4
|
import { JobInterruptOptions, JobState } from '../../types/job';
|
|
5
5
|
import { Activity } from './activity';
|
|
6
|
+
/**
|
|
7
|
+
* Terminates a flow by sending an interrupt signal. The `interrupt` activity
|
|
8
|
+
* can target the current flow (self-interrupt) or any other flow by its
|
|
9
|
+
* job ID (remote interrupt). Interrupted jobs have their status set to a
|
|
10
|
+
* value less than -100,000,000, indicating abnormal termination.
|
|
11
|
+
*
|
|
12
|
+
* ## YAML Configuration — Self-Interrupt
|
|
13
|
+
*
|
|
14
|
+
* When no `target` is specified, the activity interrupts the current flow.
|
|
15
|
+
* The flow terminates immediately after this activity executes. Use
|
|
16
|
+
* conditional transitions to route to an interrupt only when needed.
|
|
17
|
+
*
|
|
18
|
+
* ```yaml
|
|
19
|
+
* app:
|
|
20
|
+
* id: myapp
|
|
21
|
+
* version: '1'
|
|
22
|
+
* graphs:
|
|
23
|
+
* - subscribes: validation.check
|
|
24
|
+
* expire: 120
|
|
25
|
+
*
|
|
26
|
+
* activities:
|
|
27
|
+
* t1:
|
|
28
|
+
* type: trigger
|
|
29
|
+
*
|
|
30
|
+
* validate:
|
|
31
|
+
* type: worker
|
|
32
|
+
* topic: validate.input
|
|
33
|
+
*
|
|
34
|
+
* cancel:
|
|
35
|
+
* type: interrupt
|
|
36
|
+
* reason: 'Validation failed'
|
|
37
|
+
* throw: true
|
|
38
|
+
* code: 410
|
|
39
|
+
* job:
|
|
40
|
+
* maps:
|
|
41
|
+
* cancelled_at: '{$self.output.metadata.ac}'
|
|
42
|
+
*
|
|
43
|
+
* proceed:
|
|
44
|
+
* type: hook
|
|
45
|
+
*
|
|
46
|
+
* transitions:
|
|
47
|
+
* t1:
|
|
48
|
+
* - to: validate
|
|
49
|
+
* validate:
|
|
50
|
+
* - to: cancel
|
|
51
|
+
* conditions:
|
|
52
|
+
* code: 422 # interrupt only on validation failure
|
|
53
|
+
* - to: proceed
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* ## YAML Configuration — Remote Interrupt
|
|
57
|
+
*
|
|
58
|
+
* When `target` is specified, the activity interrupts another flow while
|
|
59
|
+
* the current flow continues to transition to adjacent activities.
|
|
60
|
+
*
|
|
61
|
+
* ```yaml
|
|
62
|
+
* app:
|
|
63
|
+
* id: myapp
|
|
64
|
+
* version: '1'
|
|
65
|
+
* graphs:
|
|
66
|
+
* - subscribes: parent.flow
|
|
67
|
+
* expire: 120
|
|
68
|
+
*
|
|
69
|
+
* activities:
|
|
70
|
+
* t1:
|
|
71
|
+
* type: trigger
|
|
72
|
+
* job:
|
|
73
|
+
* maps:
|
|
74
|
+
* childJobId: '{$self.output.data.childJobId}'
|
|
75
|
+
*
|
|
76
|
+
* stop_child:
|
|
77
|
+
* type: interrupt
|
|
78
|
+
* topic: child.flow.topic # topic of the target flow
|
|
79
|
+
* target: '{t1.output.data.childJobId}'
|
|
80
|
+
* throw: false # do not throw (silent cancellation)
|
|
81
|
+
* descend: true # also interrupt descendant sub-flows
|
|
82
|
+
* job:
|
|
83
|
+
* maps:
|
|
84
|
+
* interrupted: true
|
|
85
|
+
*
|
|
86
|
+
* done:
|
|
87
|
+
* type: hook
|
|
88
|
+
*
|
|
89
|
+
* transitions:
|
|
90
|
+
* t1:
|
|
91
|
+
* - to: stop_child
|
|
92
|
+
* stop_child:
|
|
93
|
+
* - to: done
|
|
94
|
+
* ```
|
|
95
|
+
*
|
|
96
|
+
* ## Configuration Properties
|
|
97
|
+
*
|
|
98
|
+
* | Property | Type | Default | Description |
|
|
99
|
+
* |------------|---------|--------------------|-------------|
|
|
100
|
+
* | `target` | string | (current job) | Job ID to interrupt. Supports `@pipe` expressions. |
|
|
101
|
+
* | `topic` | string | (current topic) | Topic of the target flow |
|
|
102
|
+
* | `reason` | string | `'Job Interrupted'`| Error message attached to the interruption |
|
|
103
|
+
* | `throw` | boolean | `true` | Whether to throw a `JobInterrupted` error |
|
|
104
|
+
* | `descend` | boolean | `false` | Whether to cascade to child/descendant flows |
|
|
105
|
+
* | `code` | number | `410` | Error code attached to the interruption |
|
|
106
|
+
* | `stack` | string | — | Optional stack trace |
|
|
107
|
+
*
|
|
108
|
+
* ## Execution Model
|
|
109
|
+
*
|
|
110
|
+
* - **Self-interrupt (no `target`)**: Category C. Verifies entry, maps job
|
|
111
|
+
* data, sets status to -1, and fires the interrupt. No children.
|
|
112
|
+
* - **Remote interrupt (with `target`)**: Category B. Fires the interrupt
|
|
113
|
+
* best-effort, then uses `executeLeg1StepProtocol` to transition to
|
|
114
|
+
* adjacent activities.
|
|
115
|
+
*
|
|
116
|
+
* @see {@link InterruptActivity} for the TypeScript interface
|
|
117
|
+
*/
|
|
6
118
|
declare class Interrupt extends Activity {
|
|
7
119
|
config: InterruptActivity;
|
|
8
120
|
constructor(config: ActivityType, data: ActivityData, metadata: ActivityMetadata, hook: ActivityData | null, engine: EngineService, context?: JobState);
|
|
@@ -6,6 +6,118 @@ const collator_1 = require("../collator");
|
|
|
6
6
|
const pipe_1 = require("../pipe");
|
|
7
7
|
const telemetry_1 = require("../telemetry");
|
|
8
8
|
const activity_1 = require("./activity");
|
|
9
|
+
/**
|
|
10
|
+
* Terminates a flow by sending an interrupt signal. The `interrupt` activity
|
|
11
|
+
* can target the current flow (self-interrupt) or any other flow by its
|
|
12
|
+
* job ID (remote interrupt). Interrupted jobs have their status set to a
|
|
13
|
+
* value less than -100,000,000, indicating abnormal termination.
|
|
14
|
+
*
|
|
15
|
+
* ## YAML Configuration — Self-Interrupt
|
|
16
|
+
*
|
|
17
|
+
* When no `target` is specified, the activity interrupts the current flow.
|
|
18
|
+
* The flow terminates immediately after this activity executes. Use
|
|
19
|
+
* conditional transitions to route to an interrupt only when needed.
|
|
20
|
+
*
|
|
21
|
+
* ```yaml
|
|
22
|
+
* app:
|
|
23
|
+
* id: myapp
|
|
24
|
+
* version: '1'
|
|
25
|
+
* graphs:
|
|
26
|
+
* - subscribes: validation.check
|
|
27
|
+
* expire: 120
|
|
28
|
+
*
|
|
29
|
+
* activities:
|
|
30
|
+
* t1:
|
|
31
|
+
* type: trigger
|
|
32
|
+
*
|
|
33
|
+
* validate:
|
|
34
|
+
* type: worker
|
|
35
|
+
* topic: validate.input
|
|
36
|
+
*
|
|
37
|
+
* cancel:
|
|
38
|
+
* type: interrupt
|
|
39
|
+
* reason: 'Validation failed'
|
|
40
|
+
* throw: true
|
|
41
|
+
* code: 410
|
|
42
|
+
* job:
|
|
43
|
+
* maps:
|
|
44
|
+
* cancelled_at: '{$self.output.metadata.ac}'
|
|
45
|
+
*
|
|
46
|
+
* proceed:
|
|
47
|
+
* type: hook
|
|
48
|
+
*
|
|
49
|
+
* transitions:
|
|
50
|
+
* t1:
|
|
51
|
+
* - to: validate
|
|
52
|
+
* validate:
|
|
53
|
+
* - to: cancel
|
|
54
|
+
* conditions:
|
|
55
|
+
* code: 422 # interrupt only on validation failure
|
|
56
|
+
* - to: proceed
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* ## YAML Configuration — Remote Interrupt
|
|
60
|
+
*
|
|
61
|
+
* When `target` is specified, the activity interrupts another flow while
|
|
62
|
+
* the current flow continues to transition to adjacent activities.
|
|
63
|
+
*
|
|
64
|
+
* ```yaml
|
|
65
|
+
* app:
|
|
66
|
+
* id: myapp
|
|
67
|
+
* version: '1'
|
|
68
|
+
* graphs:
|
|
69
|
+
* - subscribes: parent.flow
|
|
70
|
+
* expire: 120
|
|
71
|
+
*
|
|
72
|
+
* activities:
|
|
73
|
+
* t1:
|
|
74
|
+
* type: trigger
|
|
75
|
+
* job:
|
|
76
|
+
* maps:
|
|
77
|
+
* childJobId: '{$self.output.data.childJobId}'
|
|
78
|
+
*
|
|
79
|
+
* stop_child:
|
|
80
|
+
* type: interrupt
|
|
81
|
+
* topic: child.flow.topic # topic of the target flow
|
|
82
|
+
* target: '{t1.output.data.childJobId}'
|
|
83
|
+
* throw: false # do not throw (silent cancellation)
|
|
84
|
+
* descend: true # also interrupt descendant sub-flows
|
|
85
|
+
* job:
|
|
86
|
+
* maps:
|
|
87
|
+
* interrupted: true
|
|
88
|
+
*
|
|
89
|
+
* done:
|
|
90
|
+
* type: hook
|
|
91
|
+
*
|
|
92
|
+
* transitions:
|
|
93
|
+
* t1:
|
|
94
|
+
* - to: stop_child
|
|
95
|
+
* stop_child:
|
|
96
|
+
* - to: done
|
|
97
|
+
* ```
|
|
98
|
+
*
|
|
99
|
+
* ## Configuration Properties
|
|
100
|
+
*
|
|
101
|
+
* | Property | Type | Default | Description |
|
|
102
|
+
* |------------|---------|--------------------|-------------|
|
|
103
|
+
* | `target` | string | (current job) | Job ID to interrupt. Supports `@pipe` expressions. |
|
|
104
|
+
* | `topic` | string | (current topic) | Topic of the target flow |
|
|
105
|
+
* | `reason` | string | `'Job Interrupted'`| Error message attached to the interruption |
|
|
106
|
+
* | `throw` | boolean | `true` | Whether to throw a `JobInterrupted` error |
|
|
107
|
+
* | `descend` | boolean | `false` | Whether to cascade to child/descendant flows |
|
|
108
|
+
* | `code` | number | `410` | Error code attached to the interruption |
|
|
109
|
+
* | `stack` | string | — | Optional stack trace |
|
|
110
|
+
*
|
|
111
|
+
* ## Execution Model
|
|
112
|
+
*
|
|
113
|
+
* - **Self-interrupt (no `target`)**: Category C. Verifies entry, maps job
|
|
114
|
+
* data, sets status to -1, and fires the interrupt. No children.
|
|
115
|
+
* - **Remote interrupt (with `target`)**: Category B. Fires the interrupt
|
|
116
|
+
* best-effort, then uses `executeLeg1StepProtocol` to transition to
|
|
117
|
+
* adjacent activities.
|
|
118
|
+
*
|
|
119
|
+
* @see {@link InterruptActivity} for the TypeScript interface
|
|
120
|
+
*/
|
|
9
121
|
class Interrupt extends activity_1.Activity {
|
|
10
122
|
constructor(config, data, metadata, hook, engine, context) {
|
|
11
123
|
super(config, data, metadata, hook, engine, context);
|
|
@@ -19,13 +131,18 @@ class Interrupt extends activity_1.Activity {
|
|
|
19
131
|
});
|
|
20
132
|
let telemetry;
|
|
21
133
|
try {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
134
|
+
if (!this.config.target) {
|
|
135
|
+
//Category C: self-interrupt (no children, no semaphore edge risk)
|
|
136
|
+
await this.verifyEntry();
|
|
137
|
+
telemetry = new telemetry_1.TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
|
|
138
|
+
telemetry.startActivitySpan(this.leg);
|
|
26
139
|
await this.interruptSelf(telemetry);
|
|
27
140
|
}
|
|
28
141
|
else {
|
|
142
|
+
//Category B: interrupt another (spawns children, needs step protocol)
|
|
143
|
+
await this.verifyLeg1Entry();
|
|
144
|
+
telemetry = new telemetry_1.TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
|
|
145
|
+
telemetry.startActivitySpan(this.leg);
|
|
29
146
|
await this.interruptAnother(telemetry);
|
|
30
147
|
}
|
|
31
148
|
}
|
|
@@ -69,20 +186,21 @@ class Interrupt extends activity_1.Activity {
|
|
|
69
186
|
}
|
|
70
187
|
}
|
|
71
188
|
async interruptSelf(telemetry) {
|
|
72
|
-
// Apply final updates to THIS job's state
|
|
73
189
|
if (this.config.job?.maps) {
|
|
74
190
|
this.mapJobData();
|
|
75
|
-
await this.setState();
|
|
76
191
|
}
|
|
77
|
-
//
|
|
78
|
-
const messageId = await this.interrupt();
|
|
79
|
-
// Notarize completion and log
|
|
192
|
+
// Bundle state + Leg1 completion + semaphore in one transaction
|
|
80
193
|
telemetry.mapActivityAttributes();
|
|
81
194
|
const transaction = this.store.transact();
|
|
82
|
-
|
|
195
|
+
if (this.config.job?.maps) {
|
|
196
|
+
await this.setState(transaction);
|
|
197
|
+
}
|
|
198
|
+
await collator_1.CollatorService.notarizeLeg1Completion(this, transaction);
|
|
83
199
|
await this.setStatus(-1, transaction);
|
|
84
200
|
const txResponse = (await transaction.exec());
|
|
85
201
|
const jobStatus = this.resolveStatus(txResponse);
|
|
202
|
+
// Interrupt fires AFTER proof commits (best-effort)
|
|
203
|
+
const messageId = await this.interrupt();
|
|
86
204
|
telemetry.setActivityAttributes({
|
|
87
205
|
'app.activity.mid': messageId,
|
|
88
206
|
'app.job.jss': jobStatus,
|
|
@@ -90,31 +208,18 @@ class Interrupt extends activity_1.Activity {
|
|
|
90
208
|
return this.context.metadata.aid;
|
|
91
209
|
}
|
|
92
210
|
async interruptAnother(telemetry) {
|
|
93
|
-
// Interrupt ANOTHER job
|
|
94
|
-
|
|
95
|
-
const attrs = { 'app.activity.mid': messageId };
|
|
211
|
+
// Interrupt ANOTHER job (best-effort, fires before step protocol)
|
|
212
|
+
await this.interrupt();
|
|
96
213
|
// Apply updates to THIS job's state
|
|
97
|
-
telemetry.mapActivityAttributes();
|
|
98
214
|
this.adjacencyList = await this.filterAdjacent();
|
|
99
215
|
if (this.config.job?.maps || this.config.output?.maps) {
|
|
100
216
|
this.mapOutputData();
|
|
101
217
|
this.mapJobData();
|
|
102
|
-
const transaction = this.store.transact();
|
|
103
|
-
await this.setState(transaction);
|
|
104
|
-
}
|
|
105
|
-
// Notarize completion
|
|
106
|
-
const transaction = this.store.transact();
|
|
107
|
-
await collator_1.CollatorService.notarizeEarlyCompletion(this, transaction);
|
|
108
|
-
await this.setStatus(this.adjacencyList.length - 1, transaction);
|
|
109
|
-
const txResponse = (await transaction.exec());
|
|
110
|
-
const jobStatus = this.resolveStatus(txResponse);
|
|
111
|
-
attrs['app.job.jss'] = jobStatus;
|
|
112
|
-
// Transition next generation and log
|
|
113
|
-
const messageIds = await this.transition(this.adjacencyList, jobStatus);
|
|
114
|
-
if (messageIds.length) {
|
|
115
|
-
attrs['app.activity.mids'] = messageIds.join(',');
|
|
116
218
|
}
|
|
117
|
-
|
|
219
|
+
//Category B: use Leg1 step protocol for crash-safe edge capture
|
|
220
|
+
await this.executeLeg1StepProtocol(this.adjacencyList.length - 1);
|
|
221
|
+
telemetry.mapActivityAttributes();
|
|
222
|
+
telemetry.setActivityAttributes({});
|
|
118
223
|
return this.context.metadata.aid;
|
|
119
224
|
}
|
|
120
225
|
isInterruptingSelf() {
|
|
@@ -1,7 +1,113 @@
|
|
|
1
1
|
import { EngineService } from '../engine';
|
|
2
2
|
import { ActivityData, ActivityMetadata, ActivityType, SignalActivity } from '../../types/activity';
|
|
3
3
|
import { JobState } from '../../types/job';
|
|
4
|
+
import { ProviderTransaction } from '../../types/provider';
|
|
4
5
|
import { Activity } from './activity';
|
|
6
|
+
/**
|
|
7
|
+
* Sends a signal to one or more paused flows, resuming their execution.
|
|
8
|
+
* The `signal` activity is the counterpart to a `Hook` activity
|
|
9
|
+
* configured with a webhook listener. It allows any flow to reach into
|
|
10
|
+
* another flow and deliver data to a waiting hook, regardless of the
|
|
11
|
+
* relationship between the flows.
|
|
12
|
+
*
|
|
13
|
+
* ## YAML Configuration — Signal One
|
|
14
|
+
*
|
|
15
|
+
* Resumes a single paused flow by publishing to the hook's topic. Use
|
|
16
|
+
* `subtype: one` when you know the specific hook topic to signal.
|
|
17
|
+
*
|
|
18
|
+
* ```yaml
|
|
19
|
+
* app:
|
|
20
|
+
* id: myapp
|
|
21
|
+
* version: '1'
|
|
22
|
+
* graphs:
|
|
23
|
+
* - subscribes: signal.start
|
|
24
|
+
* expire: 120
|
|
25
|
+
*
|
|
26
|
+
* activities:
|
|
27
|
+
* t1:
|
|
28
|
+
* type: trigger
|
|
29
|
+
*
|
|
30
|
+
* resume_hook:
|
|
31
|
+
* type: signal
|
|
32
|
+
* subtype: one
|
|
33
|
+
* topic: my.hook.topic # the hook's registered topic
|
|
34
|
+
* status: success # optional: success (default) or pending
|
|
35
|
+
* code: 200 # optional: 200 (default) or 202 (keep-alive)
|
|
36
|
+
* signal:
|
|
37
|
+
* schema:
|
|
38
|
+
* type: object
|
|
39
|
+
* properties:
|
|
40
|
+
* approved: { type: boolean }
|
|
41
|
+
* maps:
|
|
42
|
+
* approved: true # data delivered to the hook
|
|
43
|
+
*
|
|
44
|
+
* done:
|
|
45
|
+
* type: hook
|
|
46
|
+
*
|
|
47
|
+
* transitions:
|
|
48
|
+
* t1:
|
|
49
|
+
* - to: resume_hook
|
|
50
|
+
* resume_hook:
|
|
51
|
+
* - to: done
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* A `code: 202` signal delivers data but keeps the hook alive for
|
|
55
|
+
* additional signals. A `code: 200` (default) closes the hook.
|
|
56
|
+
*
|
|
57
|
+
* ## YAML Configuration — Signal All
|
|
58
|
+
*
|
|
59
|
+
* Resumes all paused flows that share a common job key facet. Use
|
|
60
|
+
* `subtype: all` for fan-out patterns where multiple waiting flows
|
|
61
|
+
* should be resumed simultaneously.
|
|
62
|
+
*
|
|
63
|
+
* ```yaml
|
|
64
|
+
* app:
|
|
65
|
+
* id: myapp
|
|
66
|
+
* version: '1'
|
|
67
|
+
* graphs:
|
|
68
|
+
* - subscribes: signal.fan.out
|
|
69
|
+
* expire: 120
|
|
70
|
+
*
|
|
71
|
+
* activities:
|
|
72
|
+
* t1:
|
|
73
|
+
* type: trigger
|
|
74
|
+
*
|
|
75
|
+
* resume_all:
|
|
76
|
+
* type: signal
|
|
77
|
+
* subtype: all
|
|
78
|
+
* topic: hook.resume
|
|
79
|
+
* key_name: parent_job_id # index facet name
|
|
80
|
+
* key_value: '{$job.metadata.jid}'
|
|
81
|
+
* scrub: true # clean up indexes after use
|
|
82
|
+
* resolver:
|
|
83
|
+
* maps:
|
|
84
|
+
* data:
|
|
85
|
+
* parent_job_id: '{$job.metadata.jid}'
|
|
86
|
+
* scrub: true
|
|
87
|
+
* signal:
|
|
88
|
+
* maps:
|
|
89
|
+
* done: true # data delivered to all matching hooks
|
|
90
|
+
*
|
|
91
|
+
* done:
|
|
92
|
+
* type: hook
|
|
93
|
+
*
|
|
94
|
+
* transitions:
|
|
95
|
+
* t1:
|
|
96
|
+
* - to: resume_all
|
|
97
|
+
* resume_all:
|
|
98
|
+
* - to: done
|
|
99
|
+
* ```
|
|
100
|
+
*
|
|
101
|
+
* ## Execution Model
|
|
102
|
+
*
|
|
103
|
+
* Signal is a **Category B (Leg1-only with children)** activity:
|
|
104
|
+
* - Bundles the hook signal with the Leg 1 completion marker in a
|
|
105
|
+
* single transaction (`hookOne`) or fires best-effort (`hookAll`).
|
|
106
|
+
* - Executes the crash-safe `executeLeg1StepProtocol` to transition
|
|
107
|
+
* to adjacent activities.
|
|
108
|
+
*
|
|
109
|
+
* @see {@link SignalActivity} for the TypeScript interface
|
|
110
|
+
*/
|
|
5
111
|
declare class Signal extends Activity {
|
|
6
112
|
config: SignalActivity;
|
|
7
113
|
constructor(config: ActivityType, data: ActivityData, metadata: ActivityMetadata, hook: ActivityData | null, engine: EngineService, context?: JobState);
|
|
@@ -9,12 +115,13 @@ declare class Signal extends Activity {
|
|
|
9
115
|
mapSignalData(): Record<string, any>;
|
|
10
116
|
mapResolverData(): Record<string, any>;
|
|
11
117
|
/**
|
|
12
|
-
* The signal activity will hook one
|
|
118
|
+
* The signal activity will hook one. Accepts an optional transaction
|
|
119
|
+
* so the hook publish can be bundled with the Leg1 completion marker.
|
|
13
120
|
*/
|
|
14
|
-
|
|
121
|
+
signalOne(transaction?: ProviderTransaction): Promise<string>;
|
|
15
122
|
/**
|
|
16
|
-
*
|
|
123
|
+
* Signals all paused jobs that share the same job key, resuming their execution.
|
|
17
124
|
*/
|
|
18
|
-
|
|
125
|
+
signalAll(): Promise<string[]>;
|
|
19
126
|
}
|
|
20
127
|
export { Signal };
|