@hotmeshio/hotmesh 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +2 -1
- package/README.md +158 -38
- package/build/package.json +62 -67
- package/build/services/activities/activity.d.ts +58 -7
- package/build/services/activities/activity.js +66 -37
- package/build/services/activities/await.d.ts +101 -0
- package/build/services/activities/await.js +101 -0
- package/build/services/activities/cycle.d.ts +82 -0
- package/build/services/activities/cycle.js +86 -8
- package/build/services/activities/hook.d.ts +139 -1
- package/build/services/activities/hook.js +140 -2
- package/build/services/activities/interrupt.d.ts +112 -0
- package/build/services/activities/interrupt.js +118 -5
- package/build/services/activities/signal.d.ts +108 -3
- package/build/services/activities/signal.js +113 -8
- package/build/services/activities/trigger.d.ts +56 -4
- package/build/services/activities/trigger.js +119 -35
- package/build/services/activities/worker.d.ts +107 -0
- package/build/services/activities/worker.js +107 -0
- package/build/services/collator/index.d.ts +3 -15
- package/build/services/collator/index.js +7 -34
- package/build/services/engine/index.d.ts +18 -2
- package/build/services/engine/index.js +14 -4
- package/build/services/exporter/index.d.ts +2 -0
- package/build/services/exporter/index.js +1 -0
- package/build/services/hotmesh/index.d.ts +471 -236
- package/build/services/hotmesh/index.js +473 -238
- package/build/services/memflow/client.js +2 -2
- package/build/services/memflow/handle.js +1 -1
- package/build/services/memflow/index.d.ts +1 -1
- package/build/services/memflow/index.js +1 -1
- package/build/services/memflow/workflow/all.d.ts +28 -3
- package/build/services/memflow/workflow/all.js +28 -3
- package/build/services/memflow/workflow/context.d.ts +44 -1
- package/build/services/memflow/workflow/context.js +44 -1
- package/build/services/memflow/workflow/didRun.d.ts +23 -3
- package/build/services/memflow/workflow/didRun.js +23 -3
- package/build/services/memflow/workflow/emit.d.ts +43 -4
- package/build/services/memflow/workflow/emit.js +43 -4
- package/build/services/memflow/workflow/enrich.d.ts +32 -4
- package/build/services/memflow/workflow/enrich.js +32 -4
- package/build/services/memflow/workflow/entityMethods.d.ts +54 -7
- package/build/services/memflow/workflow/entityMethods.js +54 -7
- package/build/services/memflow/workflow/execChild.d.ts +96 -8
- package/build/services/memflow/workflow/execChild.js +96 -8
- package/build/services/memflow/workflow/execHook.d.ts +54 -39
- package/build/services/memflow/workflow/execHook.js +52 -38
- package/build/services/memflow/workflow/execHookBatch.d.ts +82 -29
- package/build/services/memflow/workflow/execHookBatch.js +80 -28
- package/build/services/memflow/workflow/hook.d.ts +68 -3
- package/build/services/memflow/workflow/hook.js +69 -4
- package/build/services/memflow/workflow/index.d.ts +65 -10
- package/build/services/memflow/workflow/index.js +65 -10
- package/build/services/memflow/workflow/interrupt.d.ts +50 -4
- package/build/services/memflow/workflow/interrupt.js +50 -4
- package/build/services/memflow/workflow/interruption.d.ts +49 -16
- package/build/services/memflow/workflow/interruption.js +49 -16
- package/build/services/memflow/workflow/isSideEffectAllowed.d.ts +21 -4
- package/build/services/memflow/workflow/isSideEffectAllowed.js +21 -4
- package/build/services/memflow/workflow/proxyActivities.d.ts +70 -42
- package/build/services/memflow/workflow/proxyActivities.js +70 -42
- package/build/services/memflow/workflow/random.d.ts +33 -3
- package/build/services/memflow/workflow/random.js +33 -3
- package/build/services/memflow/workflow/searchMethods.d.ts +49 -2
- package/build/services/memflow/workflow/searchMethods.js +49 -2
- package/build/services/memflow/workflow/signal.d.ts +51 -22
- package/build/services/memflow/workflow/signal.js +52 -23
- package/build/services/memflow/workflow/sleepFor.d.ts +57 -18
- package/build/services/memflow/workflow/sleepFor.js +57 -18
- package/build/services/memflow/workflow/trace.d.ts +39 -6
- package/build/services/memflow/workflow/trace.js +39 -6
- package/build/services/memflow/workflow/waitFor.d.ts +55 -18
- package/build/services/memflow/workflow/waitFor.js +55 -18
- package/build/services/store/index.d.ts +1 -1
- package/build/services/store/providers/postgres/postgres.d.ts +1 -1
- package/build/services/store/providers/postgres/postgres.js +4 -3
- package/build/services/telemetry/index.js +6 -0
- package/build/types/activity.d.ts +1 -1
- package/build/types/hotmesh.d.ts +1 -1
- package/build/types/job.d.ts +1 -1
- package/build/types/memflow.d.ts +1 -1
- package/build/types/quorum.d.ts +2 -2
- package/build/vitest.config.d.ts +2 -0
- package/build/vitest.config.js +18 -0
- package/package.json +62 -67
- package/vitest.config.ts +17 -0
|
@@ -11,7 +11,62 @@ const serializer_1 = require("../serializer");
|
|
|
11
11
|
const telemetry_1 = require("../telemetry");
|
|
12
12
|
const stream_1 = require("../../types/stream");
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
14
|
+
* Base class for all HotMesh activity types. Activities are the execution
|
|
15
|
+
* units within a YAML-defined workflow graph. Each activity represents a
|
|
16
|
+
* node in a Directed Acyclic Graph (DAG) that the engine orchestrates.
|
|
17
|
+
*
|
|
18
|
+
* ## Activity Categories
|
|
19
|
+
*
|
|
20
|
+
* Activities fall into three execution categories:
|
|
21
|
+
*
|
|
22
|
+
* - **Category A (Duplex)**: Two-phase activities with Leg 1 (dispatch) and
|
|
23
|
+
* Leg 2 (response). Used by `Worker` and `Await`. Leg 1
|
|
24
|
+
* publishes a message and waits; Leg 2 handles the response via
|
|
25
|
+
* `processEvent` and transitions to adjacent activities.
|
|
26
|
+
*
|
|
27
|
+
* - **Category B (Leg1-only with children)**: Single-phase activities that
|
|
28
|
+
* execute work and transition to children using the crash-safe
|
|
29
|
+
* `executeLeg1StepProtocol`. Used by `Hook` (passthrough mode),
|
|
30
|
+
* `Signal`, and `Interrupt` (target mode).
|
|
31
|
+
*
|
|
32
|
+
* - **Category C (Leg1-only, no children)**: Terminal activities that
|
|
33
|
+
* execute without spawning children. Used by `Interrupt` (self mode).
|
|
34
|
+
*
|
|
35
|
+
* ## Shared YAML Configuration
|
|
36
|
+
*
|
|
37
|
+
* All activity types support these base properties in the YAML descriptor:
|
|
38
|
+
*
|
|
39
|
+
* | Property | Type | Description |
|
|
40
|
+
* |----------------------|---------|-------------|
|
|
41
|
+
* | `type` | string | Activity type: `trigger`, `worker`, `await`, `hook`, `signal`, `interrupt`, `cycle` |
|
|
42
|
+
* | `title` | string | Human-readable label for the activity |
|
|
43
|
+
* | `input.schema` | object | JSON Schema for input validation |
|
|
44
|
+
* | `input.maps` | object | Maps data from other activities into this activity's input |
|
|
45
|
+
* | `output.schema` | object | JSON Schema for output validation |
|
|
46
|
+
* | `output.maps` | object | Maps/transforms the activity's own output data |
|
|
47
|
+
* | `job.maps` | object | Maps activity data to the shared job state |
|
|
48
|
+
* | `emit` | boolean | If `true`, emits a message to the graph's `publishes` topic |
|
|
49
|
+
* | `persist` | boolean | If `true`, emits the job-completed event while keeping the job active |
|
|
50
|
+
* | `expire` | number | Seconds until the job expires after completion (`-1` = forever) |
|
|
51
|
+
* | `statusThreshold` | number | Custom semaphore threshold for Dynamic Activation Control |
|
|
52
|
+
* | `cycle` | boolean | If `true`, leaves Leg 2 open so the activity can be re-entered |
|
|
53
|
+
*
|
|
54
|
+
* ## Data Mapping Syntax
|
|
55
|
+
*
|
|
56
|
+
* Mapping expressions use curly-brace references to bind data between
|
|
57
|
+
* activities and the shared job state:
|
|
58
|
+
*
|
|
59
|
+
* ```yaml
|
|
60
|
+
* input:
|
|
61
|
+
* maps:
|
|
62
|
+
* x: '{t1.output.data.fieldName}' # reference another activity's output
|
|
63
|
+
* y: '{$self.output.data.fieldName}' # reference own output
|
|
64
|
+
* z: '{$job.data.fieldName}' # reference shared job state
|
|
65
|
+
* s: '{$app.settings.configKey}' # reference app-level settings
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
* @see {@link https://hotmeshio.github.io/sdk-typescript/docs/quickstart | Quick Start Guide}
|
|
69
|
+
* @see [Model Driven Development](https://hotmeshio.github.io/sdk-typescript/docs/model_driven_development)
|
|
15
70
|
*/
|
|
16
71
|
class Activity {
|
|
17
72
|
constructor(config, data, metadata, hook, engine, context) {
|
|
@@ -57,13 +112,15 @@ class Activity {
|
|
|
57
112
|
}
|
|
58
113
|
catch (error) {
|
|
59
114
|
await collator_1.CollatorService.notarizeEntry(this);
|
|
60
|
-
//todo: confirm this check is still needed; the edge event cleanup should handle fully
|
|
61
115
|
if (threshold > 0) {
|
|
62
|
-
if (this.context.metadata.js
|
|
63
|
-
//
|
|
116
|
+
if (this.context.metadata.js <= threshold) {
|
|
117
|
+
//Dynamic Activation Control: convergent claim — only the
|
|
118
|
+
//activity whose HINCRBY reaches exactly 0 runs completion.
|
|
64
119
|
const status = await this.setStatus(-threshold);
|
|
65
120
|
if (Number(status) === 0) {
|
|
66
|
-
|
|
121
|
+
const txn = this.store.transact();
|
|
122
|
+
await this.engine.runJobCompletionTasks(this.context, {}, txn);
|
|
123
|
+
await txn.exec();
|
|
67
124
|
}
|
|
68
125
|
}
|
|
69
126
|
}
|
|
@@ -486,13 +543,10 @@ class Activity {
|
|
|
486
543
|
const { id: appId } = await this.engine.getVID();
|
|
487
544
|
return await this.store.setStatus(amount, this.context.metadata.jid, appId, transaction);
|
|
488
545
|
}
|
|
489
|
-
authorizeEntry(
|
|
490
|
-
//
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
state[`${aid}/output/metadata/as`] = collator_1.CollatorService.getSeed();
|
|
494
|
-
return aid;
|
|
495
|
-
}) ?? []);
|
|
546
|
+
authorizeEntry(_state) {
|
|
547
|
+
//seed writes removed: child activities increment from 0 (null field).
|
|
548
|
+
//FINALIZE (200T) sets pos 1 directly to 2 without needing a 100T base.
|
|
549
|
+
return [];
|
|
496
550
|
}
|
|
497
551
|
bindDimensionalAddress(state) {
|
|
498
552
|
const dad = this.resolveDad();
|
|
@@ -721,31 +775,6 @@ class Activity {
|
|
|
721
775
|
}
|
|
722
776
|
return false;
|
|
723
777
|
}
|
|
724
|
-
/**
|
|
725
|
-
* Transition method for Category C (Leg1-only, no children, no semaphore change)
|
|
726
|
-
* and Category D (Trigger) activities. NOT used by the Leg2 step protocol.
|
|
727
|
-
*/
|
|
728
|
-
async transition(adjacencyList, jobStatus) {
|
|
729
|
-
if (this.jobWasInterrupted(jobStatus)) {
|
|
730
|
-
return;
|
|
731
|
-
}
|
|
732
|
-
let mIds = [];
|
|
733
|
-
if (this.shouldEmit() ||
|
|
734
|
-
this.isJobComplete(jobStatus) ||
|
|
735
|
-
this.shouldPersistJob()) {
|
|
736
|
-
await this.engine.runJobCompletionTasks(this.context, {
|
|
737
|
-
emit: !this.isJobComplete(jobStatus) && !this.shouldPersistJob(),
|
|
738
|
-
});
|
|
739
|
-
}
|
|
740
|
-
if (adjacencyList.length && !this.isJobComplete(jobStatus)) {
|
|
741
|
-
const transaction = this.store.transact();
|
|
742
|
-
for (const execSignal of adjacencyList) {
|
|
743
|
-
await this.engine.router?.publishMessage(null, execSignal, transaction);
|
|
744
|
-
}
|
|
745
|
-
mIds = (await transaction.exec());
|
|
746
|
-
}
|
|
747
|
-
return mIds;
|
|
748
|
-
}
|
|
749
778
|
/**
|
|
750
779
|
* A job with a vale < -100_000_000 is considered interrupted,
|
|
751
780
|
* as the interruption event decrements the job status by 1billion.
|
|
@@ -3,6 +3,107 @@ import { ActivityData, ActivityMetadata, AwaitActivity, ActivityType } from '../
|
|
|
3
3
|
import { ProviderTransaction } from '../../types/provider';
|
|
4
4
|
import { JobState } from '../../types/job';
|
|
5
5
|
import { Activity } from './activity';
|
|
6
|
+
/**
|
|
7
|
+
* Invokes another graph (sub-flow) and optionally waits for its completion.
|
|
8
|
+
* The `await` activity enables compositional workflows where one graph
|
|
9
|
+
* triggers another by publishing to its `subscribes` topic, creating a
|
|
10
|
+
* parent-child relationship between flows.
|
|
11
|
+
*
|
|
12
|
+
* ## YAML Configuration
|
|
13
|
+
*
|
|
14
|
+
* The `topic` in the await activity must match the `subscribes` topic of
|
|
15
|
+
* the child graph. Both graphs are defined in the same app YAML:
|
|
16
|
+
*
|
|
17
|
+
* ```yaml
|
|
18
|
+
* app:
|
|
19
|
+
* id: myapp
|
|
20
|
+
* version: '1'
|
|
21
|
+
* graphs:
|
|
22
|
+
*
|
|
23
|
+
* # ── Parent graph ──────────────────────────────
|
|
24
|
+
* - subscribes: order.placed
|
|
25
|
+
* expire: 120
|
|
26
|
+
*
|
|
27
|
+
* activities:
|
|
28
|
+
* t1:
|
|
29
|
+
* type: trigger
|
|
30
|
+
* job:
|
|
31
|
+
* maps:
|
|
32
|
+
* orderId: '{$self.output.data.id}'
|
|
33
|
+
*
|
|
34
|
+
* a1:
|
|
35
|
+
* type: await
|
|
36
|
+
* topic: approval.requested # ◄── targets the child graph's subscribes
|
|
37
|
+
* await: true
|
|
38
|
+
* input:
|
|
39
|
+
* schema:
|
|
40
|
+
* type: object
|
|
41
|
+
* properties:
|
|
42
|
+
* orderId: { type: string }
|
|
43
|
+
* maps:
|
|
44
|
+
* orderId: '{t1.output.data.id}'
|
|
45
|
+
* output:
|
|
46
|
+
* schema:
|
|
47
|
+
* type: object
|
|
48
|
+
* properties:
|
|
49
|
+
* approved: { type: boolean }
|
|
50
|
+
* job:
|
|
51
|
+
* maps:
|
|
52
|
+
* approval: '{$self.output.data.approved}'
|
|
53
|
+
*
|
|
54
|
+
* done:
|
|
55
|
+
* type: hook
|
|
56
|
+
*
|
|
57
|
+
* transitions:
|
|
58
|
+
* t1:
|
|
59
|
+
* - to: a1
|
|
60
|
+
* a1:
|
|
61
|
+
* - to: done
|
|
62
|
+
*
|
|
63
|
+
* # ── Child graph (invoked by the await) ────────
|
|
64
|
+
* - subscribes: approval.requested # ◄── matched by the await activity's topic
|
|
65
|
+
* publishes: approval.completed
|
|
66
|
+
* expire: 60
|
|
67
|
+
*
|
|
68
|
+
* activities:
|
|
69
|
+
* t1:
|
|
70
|
+
* type: trigger
|
|
71
|
+
* review:
|
|
72
|
+
* type: worker
|
|
73
|
+
* topic: approval.review
|
|
74
|
+
*
|
|
75
|
+
* transitions:
|
|
76
|
+
* t1:
|
|
77
|
+
* - to: review
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* ## Fire-and-Forget Mode
|
|
81
|
+
*
|
|
82
|
+
* When `await` is explicitly set to `false`, the activity starts the child
|
|
83
|
+
* flow but does not wait for its completion. The parent flow immediately
|
|
84
|
+
* continues. The child's `job_id` is returned as the output.
|
|
85
|
+
*
|
|
86
|
+
* ```yaml
|
|
87
|
+
* a1:
|
|
88
|
+
* type: await
|
|
89
|
+
* topic: background.process
|
|
90
|
+
* await: false
|
|
91
|
+
* job:
|
|
92
|
+
* maps:
|
|
93
|
+
* childJobId: '{$self.output.data.job_id}'
|
|
94
|
+
* ```
|
|
95
|
+
*
|
|
96
|
+
* ## Execution Model
|
|
97
|
+
*
|
|
98
|
+
* Await is a **Category A (duplex)** activity:
|
|
99
|
+
* - **Leg 1** (`process`): Maps input data and publishes a
|
|
100
|
+
* `StreamDataType.AWAIT` message to the engine stream. The engine
|
|
101
|
+
* starts the child flow.
|
|
102
|
+
* - **Leg 2** (`processEvent`, inherited): Receives the child flow's
|
|
103
|
+
* final output, maps output data, and transitions to adjacent activities.
|
|
104
|
+
*
|
|
105
|
+
* @see {@link AwaitActivity} for the TypeScript interface
|
|
106
|
+
*/
|
|
6
107
|
declare class Await extends Activity {
|
|
7
108
|
config: AwaitActivity;
|
|
8
109
|
constructor(config: ActivityType, data: ActivityData, metadata: ActivityMetadata, hook: ActivityData | null, engine: EngineService, context?: JobState);
|
|
@@ -8,6 +8,107 @@ const pipe_1 = require("../pipe");
|
|
|
8
8
|
const telemetry_1 = require("../telemetry");
|
|
9
9
|
const stream_1 = require("../../types/stream");
|
|
10
10
|
const activity_1 = require("./activity");
|
|
11
|
+
/**
|
|
12
|
+
* Invokes another graph (sub-flow) and optionally waits for its completion.
|
|
13
|
+
* The `await` activity enables compositional workflows where one graph
|
|
14
|
+
* triggers another by publishing to its `subscribes` topic, creating a
|
|
15
|
+
* parent-child relationship between flows.
|
|
16
|
+
*
|
|
17
|
+
* ## YAML Configuration
|
|
18
|
+
*
|
|
19
|
+
* The `topic` in the await activity must match the `subscribes` topic of
|
|
20
|
+
* the child graph. Both graphs are defined in the same app YAML:
|
|
21
|
+
*
|
|
22
|
+
* ```yaml
|
|
23
|
+
* app:
|
|
24
|
+
* id: myapp
|
|
25
|
+
* version: '1'
|
|
26
|
+
* graphs:
|
|
27
|
+
*
|
|
28
|
+
* # ── Parent graph ──────────────────────────────
|
|
29
|
+
* - subscribes: order.placed
|
|
30
|
+
* expire: 120
|
|
31
|
+
*
|
|
32
|
+
* activities:
|
|
33
|
+
* t1:
|
|
34
|
+
* type: trigger
|
|
35
|
+
* job:
|
|
36
|
+
* maps:
|
|
37
|
+
* orderId: '{$self.output.data.id}'
|
|
38
|
+
*
|
|
39
|
+
* a1:
|
|
40
|
+
* type: await
|
|
41
|
+
* topic: approval.requested # ◄── targets the child graph's subscribes
|
|
42
|
+
* await: true
|
|
43
|
+
* input:
|
|
44
|
+
* schema:
|
|
45
|
+
* type: object
|
|
46
|
+
* properties:
|
|
47
|
+
* orderId: { type: string }
|
|
48
|
+
* maps:
|
|
49
|
+
* orderId: '{t1.output.data.id}'
|
|
50
|
+
* output:
|
|
51
|
+
* schema:
|
|
52
|
+
* type: object
|
|
53
|
+
* properties:
|
|
54
|
+
* approved: { type: boolean }
|
|
55
|
+
* job:
|
|
56
|
+
* maps:
|
|
57
|
+
* approval: '{$self.output.data.approved}'
|
|
58
|
+
*
|
|
59
|
+
* done:
|
|
60
|
+
* type: hook
|
|
61
|
+
*
|
|
62
|
+
* transitions:
|
|
63
|
+
* t1:
|
|
64
|
+
* - to: a1
|
|
65
|
+
* a1:
|
|
66
|
+
* - to: done
|
|
67
|
+
*
|
|
68
|
+
* # ── Child graph (invoked by the await) ────────
|
|
69
|
+
* - subscribes: approval.requested # ◄── matched by the await activity's topic
|
|
70
|
+
* publishes: approval.completed
|
|
71
|
+
* expire: 60
|
|
72
|
+
*
|
|
73
|
+
* activities:
|
|
74
|
+
* t1:
|
|
75
|
+
* type: trigger
|
|
76
|
+
* review:
|
|
77
|
+
* type: worker
|
|
78
|
+
* topic: approval.review
|
|
79
|
+
*
|
|
80
|
+
* transitions:
|
|
81
|
+
* t1:
|
|
82
|
+
* - to: review
|
|
83
|
+
* ```
|
|
84
|
+
*
|
|
85
|
+
* ## Fire-and-Forget Mode
|
|
86
|
+
*
|
|
87
|
+
* When `await` is explicitly set to `false`, the activity starts the child
|
|
88
|
+
* flow but does not wait for its completion. The parent flow immediately
|
|
89
|
+
* continues. The child's `job_id` is returned as the output.
|
|
90
|
+
*
|
|
91
|
+
* ```yaml
|
|
92
|
+
* a1:
|
|
93
|
+
* type: await
|
|
94
|
+
* topic: background.process
|
|
95
|
+
* await: false
|
|
96
|
+
* job:
|
|
97
|
+
* maps:
|
|
98
|
+
* childJobId: '{$self.output.data.job_id}'
|
|
99
|
+
* ```
|
|
100
|
+
*
|
|
101
|
+
* ## Execution Model
|
|
102
|
+
*
|
|
103
|
+
* Await is a **Category A (duplex)** activity:
|
|
104
|
+
* - **Leg 1** (`process`): Maps input data and publishes a
|
|
105
|
+
* `StreamDataType.AWAIT` message to the engine stream. The engine
|
|
106
|
+
* starts the child flow.
|
|
107
|
+
* - **Leg 2** (`processEvent`, inherited): Receives the child flow's
|
|
108
|
+
* final output, maps output data, and transitions to adjacent activities.
|
|
109
|
+
*
|
|
110
|
+
* @see {@link AwaitActivity} for the TypeScript interface
|
|
111
|
+
*/
|
|
11
112
|
class Await extends activity_1.Activity {
|
|
12
113
|
constructor(config, data, metadata, hook, engine, context) {
|
|
13
114
|
super(config, data, metadata, hook, engine, context);
|
|
@@ -3,6 +3,88 @@ import { ActivityData, ActivityMetadata, ActivityType, CycleActivity } from '../
|
|
|
3
3
|
import { ProviderTransaction } from '../../types/provider';
|
|
4
4
|
import { JobState } from '../../types/job';
|
|
5
5
|
import { Activity } from './activity';
|
|
6
|
+
/**
|
|
7
|
+
* Re-executes an ancestor activity in a new dimensional thread, enabling
|
|
8
|
+
* retry loops and iterative patterns without violating the DAG constraint.
|
|
9
|
+
* The `cycle` activity targets a specific ancestor (typically a
|
|
10
|
+
* `Hook` with `cycle: true`) and sends execution back to that point.
|
|
11
|
+
*
|
|
12
|
+
* Each cycle iteration runs in a fresh **dimensional thread** — individual
|
|
13
|
+
* activity state is isolated per iteration, while **shared job state**
|
|
14
|
+
* (`job.maps`) accumulates across iterations. This pattern enables retries,
|
|
15
|
+
* polling loops, and iterative processing.
|
|
16
|
+
*
|
|
17
|
+
* ## YAML Configuration
|
|
18
|
+
*
|
|
19
|
+
* ```yaml
|
|
20
|
+
* app:
|
|
21
|
+
* id: myapp
|
|
22
|
+
* version: '1'
|
|
23
|
+
* graphs:
|
|
24
|
+
* - subscribes: retry.start
|
|
25
|
+
* expire: 300
|
|
26
|
+
*
|
|
27
|
+
* activities:
|
|
28
|
+
* t1:
|
|
29
|
+
* type: trigger
|
|
30
|
+
*
|
|
31
|
+
* pivot:
|
|
32
|
+
* type: hook
|
|
33
|
+
* cycle: true # marks this activity as a cycle target
|
|
34
|
+
* output:
|
|
35
|
+
* maps:
|
|
36
|
+
* retryCount: 0
|
|
37
|
+
*
|
|
38
|
+
* do_work:
|
|
39
|
+
* type: worker
|
|
40
|
+
* topic: work.do
|
|
41
|
+
* output:
|
|
42
|
+
* schema:
|
|
43
|
+
* type: object
|
|
44
|
+
* properties:
|
|
45
|
+
* result: { type: string }
|
|
46
|
+
*
|
|
47
|
+
* retry:
|
|
48
|
+
* type: cycle
|
|
49
|
+
* ancestor: pivot # re-execute from this activity
|
|
50
|
+
* input:
|
|
51
|
+
* maps:
|
|
52
|
+
* retryCount: # increment retry counter each cycle
|
|
53
|
+
* '@pipe':
|
|
54
|
+
* - ['{pivot.output.data.retryCount}', 1]
|
|
55
|
+
* - ['{@math.add}']
|
|
56
|
+
*
|
|
57
|
+
* done:
|
|
58
|
+
* type: hook
|
|
59
|
+
*
|
|
60
|
+
* transitions:
|
|
61
|
+
* t1:
|
|
62
|
+
* - to: pivot
|
|
63
|
+
* pivot:
|
|
64
|
+
* - to: do_work
|
|
65
|
+
* do_work:
|
|
66
|
+
* - to: retry
|
|
67
|
+
* conditions:
|
|
68
|
+
* code: 500 # cycle on error
|
|
69
|
+
* - to: done
|
|
70
|
+
* ```
|
|
71
|
+
*
|
|
72
|
+
* ## Key Behaviors
|
|
73
|
+
*
|
|
74
|
+
* - The `ancestor` field must reference an activity with `cycle: true`.
|
|
75
|
+
* - The cycle activity's `input.maps` override the ancestor's output data
|
|
76
|
+
* for the next iteration, allowing each cycle to pass different values.
|
|
77
|
+
* - Dimensional isolation ensures parallel cycle iterations don't collide.
|
|
78
|
+
*
|
|
79
|
+
* ## Execution Model
|
|
80
|
+
*
|
|
81
|
+
* Cycle is a **Category A (Leg 1 only)** activity:
|
|
82
|
+
* - Maps input data, resolves the re-entry dimensional address, and
|
|
83
|
+
* publishes a stream message addressed to the ancestor activity.
|
|
84
|
+
* - The ancestor re-enters via its Leg 2 path in the new dimension.
|
|
85
|
+
*
|
|
86
|
+
* @see {@link CycleActivity} for the TypeScript interface
|
|
87
|
+
*/
|
|
6
88
|
declare class Cycle extends Activity {
|
|
7
89
|
config: CycleActivity;
|
|
8
90
|
constructor(config: ActivityType, data: ActivityData, metadata: ActivityMetadata, hook: ActivityData | null, engine: EngineService, context?: JobState);
|
|
@@ -6,6 +6,88 @@ const utils_1 = require("../../modules/utils");
|
|
|
6
6
|
const collator_1 = require("../collator");
|
|
7
7
|
const telemetry_1 = require("../telemetry");
|
|
8
8
|
const activity_1 = require("./activity");
|
|
9
|
+
/**
|
|
10
|
+
* Re-executes an ancestor activity in a new dimensional thread, enabling
|
|
11
|
+
* retry loops and iterative patterns without violating the DAG constraint.
|
|
12
|
+
* The `cycle` activity targets a specific ancestor (typically a
|
|
13
|
+
* `Hook` with `cycle: true`) and sends execution back to that point.
|
|
14
|
+
*
|
|
15
|
+
* Each cycle iteration runs in a fresh **dimensional thread** — individual
|
|
16
|
+
* activity state is isolated per iteration, while **shared job state**
|
|
17
|
+
* (`job.maps`) accumulates across iterations. This pattern enables retries,
|
|
18
|
+
* polling loops, and iterative processing.
|
|
19
|
+
*
|
|
20
|
+
* ## YAML Configuration
|
|
21
|
+
*
|
|
22
|
+
* ```yaml
|
|
23
|
+
* app:
|
|
24
|
+
* id: myapp
|
|
25
|
+
* version: '1'
|
|
26
|
+
* graphs:
|
|
27
|
+
* - subscribes: retry.start
|
|
28
|
+
* expire: 300
|
|
29
|
+
*
|
|
30
|
+
* activities:
|
|
31
|
+
* t1:
|
|
32
|
+
* type: trigger
|
|
33
|
+
*
|
|
34
|
+
* pivot:
|
|
35
|
+
* type: hook
|
|
36
|
+
* cycle: true # marks this activity as a cycle target
|
|
37
|
+
* output:
|
|
38
|
+
* maps:
|
|
39
|
+
* retryCount: 0
|
|
40
|
+
*
|
|
41
|
+
* do_work:
|
|
42
|
+
* type: worker
|
|
43
|
+
* topic: work.do
|
|
44
|
+
* output:
|
|
45
|
+
* schema:
|
|
46
|
+
* type: object
|
|
47
|
+
* properties:
|
|
48
|
+
* result: { type: string }
|
|
49
|
+
*
|
|
50
|
+
* retry:
|
|
51
|
+
* type: cycle
|
|
52
|
+
* ancestor: pivot # re-execute from this activity
|
|
53
|
+
* input:
|
|
54
|
+
* maps:
|
|
55
|
+
* retryCount: # increment retry counter each cycle
|
|
56
|
+
* '@pipe':
|
|
57
|
+
* - ['{pivot.output.data.retryCount}', 1]
|
|
58
|
+
* - ['{@math.add}']
|
|
59
|
+
*
|
|
60
|
+
* done:
|
|
61
|
+
* type: hook
|
|
62
|
+
*
|
|
63
|
+
* transitions:
|
|
64
|
+
* t1:
|
|
65
|
+
* - to: pivot
|
|
66
|
+
* pivot:
|
|
67
|
+
* - to: do_work
|
|
68
|
+
* do_work:
|
|
69
|
+
* - to: retry
|
|
70
|
+
* conditions:
|
|
71
|
+
* code: 500 # cycle on error
|
|
72
|
+
* - to: done
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* ## Key Behaviors
|
|
76
|
+
*
|
|
77
|
+
* - The `ancestor` field must reference an activity with `cycle: true`.
|
|
78
|
+
* - The cycle activity's `input.maps` override the ancestor's output data
|
|
79
|
+
* for the next iteration, allowing each cycle to pass different values.
|
|
80
|
+
* - Dimensional isolation ensures parallel cycle iterations don't collide.
|
|
81
|
+
*
|
|
82
|
+
* ## Execution Model
|
|
83
|
+
*
|
|
84
|
+
* Cycle is a **Category A (Leg 1 only)** activity:
|
|
85
|
+
* - Maps input data, resolves the re-entry dimensional address, and
|
|
86
|
+
* publishes a stream message addressed to the ancestor activity.
|
|
87
|
+
* - The ancestor re-enters via its Leg 2 path in the new dimension.
|
|
88
|
+
*
|
|
89
|
+
* @see {@link CycleActivity} for the TypeScript interface
|
|
90
|
+
*/
|
|
9
91
|
class Cycle extends activity_1.Activity {
|
|
10
92
|
constructor(config, data, metadata, hook, engine, context) {
|
|
11
93
|
super(config, data, metadata, hook, engine, context);
|
|
@@ -23,23 +105,19 @@ class Cycle extends activity_1.Activity {
|
|
|
23
105
|
telemetry = new telemetry_1.TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
|
|
24
106
|
telemetry.startActivitySpan(this.leg);
|
|
25
107
|
this.mapInputData();
|
|
26
|
-
//set state/status
|
|
27
|
-
|
|
108
|
+
//set state/status, cycle ancestor, and mark Leg1 complete — single transaction
|
|
109
|
+
const transaction = this.store.transact();
|
|
28
110
|
await this.setState(transaction);
|
|
29
111
|
await this.setStatus(0, transaction); //leg 1 never changes job status
|
|
112
|
+
const messageId = await this.cycleAncestorActivity(transaction);
|
|
113
|
+
await collator_1.CollatorService.notarizeLeg1Completion(this, transaction);
|
|
30
114
|
const txResponse = (await transaction.exec());
|
|
31
115
|
telemetry.mapActivityAttributes();
|
|
32
116
|
const jobStatus = this.resolveStatus(txResponse);
|
|
33
|
-
//cycle the target ancestor
|
|
34
|
-
transaction = this.store.transact();
|
|
35
|
-
const messageId = await this.cycleAncestorActivity(transaction);
|
|
36
117
|
telemetry.setActivityAttributes({
|
|
37
118
|
'app.activity.mid': messageId,
|
|
38
119
|
'app.job.jss': jobStatus,
|
|
39
120
|
});
|
|
40
|
-
//exit early (`Cycle` activities only execute Leg 1)
|
|
41
|
-
await collator_1.CollatorService.notarizeLeg1Completion(this, transaction);
|
|
42
|
-
(await transaction.exec());
|
|
43
121
|
return this.context.metadata.aid;
|
|
44
122
|
}
|
|
45
123
|
catch (error) {
|
|
@@ -7,7 +7,145 @@ import { ProviderTransaction } from '../../types/provider';
|
|
|
7
7
|
import { StreamCode, StreamStatus } from '../../types/stream';
|
|
8
8
|
import { Activity } from './activity';
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* A versatile pause/resume activity that supports three distinct patterns:
|
|
11
|
+
* **time hook** (sleep), **web hook** (external signal), and **passthrough**
|
|
12
|
+
* (immediate transition with optional data mapping).
|
|
13
|
+
*
|
|
14
|
+
* The hook activity is the most flexible activity type. Depending on its
|
|
15
|
+
* YAML configuration, it operates in one of the following modes:
|
|
16
|
+
*
|
|
17
|
+
* ## Time Hook (Sleep)
|
|
18
|
+
*
|
|
19
|
+
* Pauses the flow for a specified duration in seconds. The `sleep` value
|
|
20
|
+
* can be a literal number or a `@pipe` expression for dynamic delays
|
|
21
|
+
* (e.g., exponential backoff).
|
|
22
|
+
*
|
|
23
|
+
* ```yaml
|
|
24
|
+
* app:
|
|
25
|
+
* id: myapp
|
|
26
|
+
* version: '1'
|
|
27
|
+
* graphs:
|
|
28
|
+
* - subscribes: job.start
|
|
29
|
+
* expire: 300
|
|
30
|
+
*
|
|
31
|
+
* activities:
|
|
32
|
+
* t1:
|
|
33
|
+
* type: trigger
|
|
34
|
+
*
|
|
35
|
+
* delay:
|
|
36
|
+
* type: hook
|
|
37
|
+
* sleep: 60 # pause for 60 seconds
|
|
38
|
+
* job:
|
|
39
|
+
* maps:
|
|
40
|
+
* paused_at: '{$self.output.metadata.ac}'
|
|
41
|
+
*
|
|
42
|
+
* resume:
|
|
43
|
+
* type: hook
|
|
44
|
+
*
|
|
45
|
+
* transitions:
|
|
46
|
+
* t1:
|
|
47
|
+
* - to: delay
|
|
48
|
+
* delay:
|
|
49
|
+
* - to: resume
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* ## Web Hook (External Signal)
|
|
53
|
+
*
|
|
54
|
+
* Registers a webhook listener on a named topic. The flow pauses until
|
|
55
|
+
* an external signal is sent to the hook's topic. The signal data becomes
|
|
56
|
+
* available as `$self.hook.data`. The `hooks` section at the graph level
|
|
57
|
+
* routes incoming signals to the waiting activity.
|
|
58
|
+
*
|
|
59
|
+
* ```yaml
|
|
60
|
+
* app:
|
|
61
|
+
* id: myapp
|
|
62
|
+
* version: '1'
|
|
63
|
+
* graphs:
|
|
64
|
+
* - subscribes: order.placed
|
|
65
|
+
* expire: 3600
|
|
66
|
+
*
|
|
67
|
+
* activities:
|
|
68
|
+
* t1:
|
|
69
|
+
* type: trigger
|
|
70
|
+
*
|
|
71
|
+
* wait_for_approval:
|
|
72
|
+
* type: hook
|
|
73
|
+
* hook:
|
|
74
|
+
* type: object
|
|
75
|
+
* properties:
|
|
76
|
+
* approved: { type: boolean }
|
|
77
|
+
* job:
|
|
78
|
+
* maps:
|
|
79
|
+
* approved: '{$self.hook.data.approved}'
|
|
80
|
+
*
|
|
81
|
+
* done:
|
|
82
|
+
* type: hook
|
|
83
|
+
*
|
|
84
|
+
* transitions:
|
|
85
|
+
* t1:
|
|
86
|
+
* - to: wait_for_approval
|
|
87
|
+
* wait_for_approval:
|
|
88
|
+
* - to: done
|
|
89
|
+
*
|
|
90
|
+
* hooks:
|
|
91
|
+
* order.approval: # external topic that delivers the signal
|
|
92
|
+
* - to: wait_for_approval
|
|
93
|
+
* conditions:
|
|
94
|
+
* match:
|
|
95
|
+
* - expected: '{t1.output.data.id}'
|
|
96
|
+
* actual: '{$self.hook.data.id}'
|
|
97
|
+
* ```
|
|
98
|
+
*
|
|
99
|
+
* ## Passthrough (No Hook)
|
|
100
|
+
*
|
|
101
|
+
* When neither `sleep` nor `hook` is configured, the hook activity acts
|
|
102
|
+
* as a passthrough: it maps data and immediately transitions to children.
|
|
103
|
+
* This is useful for data transformation, convergence points, or as a
|
|
104
|
+
* cycle pivot (with `cycle: true`).
|
|
105
|
+
*
|
|
106
|
+
* ```yaml
|
|
107
|
+
* app:
|
|
108
|
+
* id: myapp
|
|
109
|
+
* version: '1'
|
|
110
|
+
* graphs:
|
|
111
|
+
* - subscribes: job.start
|
|
112
|
+
*
|
|
113
|
+
* activities:
|
|
114
|
+
* t1:
|
|
115
|
+
* type: trigger
|
|
116
|
+
*
|
|
117
|
+
* pivot:
|
|
118
|
+
* type: hook
|
|
119
|
+
* cycle: true # enables re-entry from a cycle activity
|
|
120
|
+
* output:
|
|
121
|
+
* maps:
|
|
122
|
+
* retryCount: 0
|
|
123
|
+
* job:
|
|
124
|
+
* maps:
|
|
125
|
+
* counter: '{$self.output.data.retryCount}'
|
|
126
|
+
*
|
|
127
|
+
* do_work:
|
|
128
|
+
* type: worker
|
|
129
|
+
* topic: work.do
|
|
130
|
+
*
|
|
131
|
+
* transitions:
|
|
132
|
+
* t1:
|
|
133
|
+
* - to: pivot
|
|
134
|
+
* pivot:
|
|
135
|
+
* - to: do_work
|
|
136
|
+
* ```
|
|
137
|
+
*
|
|
138
|
+
* ## Execution Model
|
|
139
|
+
*
|
|
140
|
+
* - **With `sleep` or `hook`**: Category A (duplex). Leg 1 registers the
|
|
141
|
+
* hook and saves state. Leg 2 fires when the timer expires or the
|
|
142
|
+
* external signal arrives (via `processTimeHookEvent` or
|
|
143
|
+
* `processWebHookEvent`).
|
|
144
|
+
* - **Without `sleep` or `hook`**: Category B (passthrough). Uses the
|
|
145
|
+
* crash-safe `executeLeg1StepProtocol` to map data and transition
|
|
146
|
+
* to adjacent activities.
|
|
147
|
+
*
|
|
148
|
+
* @see {@link HookActivity} for the TypeScript interface
|
|
11
149
|
*/
|
|
12
150
|
declare class Hook extends Activity {
|
|
13
151
|
config: HookActivity;
|