Skip to content

Commit c1d7c8d

Browse files
authored
Add support for "use step" functions in class instance methods (#777)
Added support for `"use step"` directive in class instance methods, allowing instance methods to be used as workflow steps. ### What changed? - Modified the SWC plugin to recognize and transform instance methods with the "use step" directive - Added registration logic for instance method steps using `ClassName.prototype.methodName` - Implemented proper serialization of class instances to preserve the `this` context across workflow/step boundaries - Added comprehensive end-to-end tests for instance method steps - Updated error handling to allow "use step" in instance methods while still preventing "use workflow" in instance methods ### How to test? The PR includes a new end-to-end test `instanceMethodStepWorkflow` that demonstrates the functionality: 1. Run the e2e tests to verify the new instance method step functionality 2. The test creates a `Counter` class with instance methods marked as steps 3. It verifies that the instance methods can be called as steps with proper serialization of the `this` context 4. It also verifies that multiple instances of the same class can be used independently ### Why make this change? This change enables a more natural object-oriented programming model when working with workflows. Previously, only static methods, standalone functions, and object methods could be marked as steps. Now, developers can create classes with instance methods that are steps, allowing for better encapsulation and more intuitive code organization. This is particularly useful for complex workflows that need to maintain state across multiple step invocations.
1 parent b5296a7 commit c1d7c8d

File tree

28 files changed

+785
-136
lines changed

28 files changed

+785
-136
lines changed

.changeset/legal-parts-happen.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@workflow/swc-plugin": patch
3+
---
4+
5+
Add support for "use step" functions in class instance methods

packages/core/e2e/e2e.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1416,6 +1416,62 @@ describe('e2e', () => {
14161416
}
14171417
);
14181418

1419+
test(
1420+
'instanceMethodStepWorkflow - instance methods with "use step" directive',
1421+
{ timeout: 60_000 },
1422+
async () => {
1423+
// This workflow tests instance methods marked with "use step".
1424+
// The Counter class has custom serialization so the `this` context
1425+
// (the Counter instance) can be serialized across the workflow/step boundary.
1426+
//
1427+
// instanceMethodStepWorkflow(5) should:
1428+
// 1. Create Counter(5)
1429+
// 2. counter.add(10) -> 5 + 10 = 15
1430+
// 3. counter.multiply(3) -> 5 * 3 = 15
1431+
// 4. counter.describe('test counter') -> { label: 'test counter', value: 5 }
1432+
// 5. Create Counter(100), call counter2.add(50) -> 100 + 50 = 150
1433+
const run = await triggerWorkflow('instanceMethodStepWorkflow', [5]);
1434+
const returnValue = await getWorkflowReturnValue(run.runId);
1435+
1436+
expect(returnValue).toEqual({
1437+
initialValue: 5,
1438+
added: 15, // 5 + 10
1439+
multiplied: 15, // 5 * 3
1440+
description: { label: 'test counter', value: 5 },
1441+
added2: 150, // 100 + 50
1442+
});
1443+
1444+
// Verify the run completed successfully
1445+
const { json: runData } = await cliInspectJson(
1446+
`runs ${run.runId} --withData`
1447+
);
1448+
expect(runData.status).toBe('completed');
1449+
expect(runData.output).toEqual({
1450+
initialValue: 5,
1451+
added: 15,
1452+
multiplied: 15,
1453+
description: { label: 'test counter', value: 5 },
1454+
added2: 150,
1455+
});
1456+
1457+
// Verify the steps were executed (should have 4 steps: add, multiply, describe, add)
1458+
const { json: steps } = await cliInspectJson(
1459+
`steps --runId ${run.runId}`
1460+
);
1461+
// Filter to only Counter instance method steps
1462+
const counterSteps = steps.filter(
1463+
(s: any) =>
1464+
s.stepName.includes('Counter#add') ||
1465+
s.stepName.includes('Counter#multiply') ||
1466+
s.stepName.includes('Counter#describe')
1467+
);
1468+
expect(counterSteps.length).toBe(4); // add, multiply, describe, add (from counter2)
1469+
expect(counterSteps.every((s: any) => s.status === 'completed')).toBe(
1470+
true
1471+
);
1472+
}
1473+
);
1474+
14191475
test(
14201476
'crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context',
14211477
{ timeout: 60_000 },

packages/swc-plugin-workflow/spec.md

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Examples:
4242
- `step//src/jobs/order.ts//fetchData`
4343
- `step//src/jobs/order.ts//processOrder/innerStep` (nested step)
4444
- `step//src/jobs/order.ts//MyClass.staticMethod` (static method)
45+
- `step//src/jobs/order.ts//MyClass#instanceMethod` (instance method)
4546
- `class//src/models/Point.ts//Point` (serialization class)
4647

4748
---
@@ -171,6 +172,57 @@ function wrapper(multiplier) {
171172
registerStepFunction("step//input.js//wrapper/_anonymousStep0", wrapper$_anonymousStep0);
172173
```
173174

175+
### Instance Method Step
176+
177+
Instance methods can use `"use step"` if the class provides custom serialization methods. The `this` context is serialized when calling the step and deserialized before execution.
178+
179+
Input:
180+
```javascript
181+
import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@vercel/workflow';
182+
183+
export class Counter {
184+
static [WORKFLOW_SERIALIZE](instance) {
185+
return { value: instance.value };
186+
}
187+
static [WORKFLOW_DESERIALIZE](data) {
188+
return new Counter(data.value);
189+
}
190+
constructor(value) {
191+
this.value = value;
192+
}
193+
async add(amount) {
194+
'use step';
195+
return this.value + amount;
196+
}
197+
}
198+
```
199+
200+
Output:
201+
```javascript
202+
import { registerStepFunction } from "workflow/internal/private";
203+
import { registerSerializationClass } from "workflow/internal/class-serialization";
204+
import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@vercel/workflow';
205+
/**__internal_workflows{"steps":{"input.js":{"Counter#add":{"stepId":"step//input.js//Counter#add"}}},"classes":{"input.js":{"Counter":{"classId":"class//input.js//Counter"}}}}*/;
206+
export class Counter {
207+
static [WORKFLOW_SERIALIZE](instance) {
208+
return { value: instance.value };
209+
}
210+
static [WORKFLOW_DESERIALIZE](data) {
211+
return new Counter(data.value);
212+
}
213+
constructor(value) {
214+
this.value = value;
215+
}
216+
async add(amount) {
217+
return this.value + amount;
218+
}
219+
}
220+
registerStepFunction("step//input.js//Counter#add", Counter.prototype["add"]);
221+
registerSerializationClass("class//input.js//Counter", Counter);
222+
```
223+
224+
Note: Instance methods use `#` in the step ID (e.g., `Counter#add`) and are registered via `ClassName.prototype["methodName"]`.
225+
174226
### Module-Level Directive
175227

176228
Input:
@@ -591,7 +643,7 @@ The plugin emits errors for invalid usage:
591643
| Error | Description |
592644
|-------|-------------|
593645
| Non-async function | Functions with `"use step"` or `"use workflow"` must be async |
594-
| Instance methods | Only static methods can have directives (not instance methods) |
646+
| Instance methods with `"use workflow"` | Only static methods can have `"use workflow"` (not instance methods) |
595647
| Misplaced directive | Directive must be at top of file or start of function body |
596648
| Conflicting directives | Cannot have both `"use step"` and `"use workflow"` at module level |
597649
| Invalid exports | Module-level directive files can only export async functions |
@@ -610,6 +662,7 @@ The plugin supports various function declaration styles:
610662
- `const name = async function() { "use step"; }` - Function expression
611663
- `{ async method() { "use step"; } }` - Object method
612664
- `static async method() { "use step"; }` - Static class method
665+
- `async method() { "use step"; }` - Instance class method (requires custom serialization)
613666

614667
---
615668

0 commit comments

Comments
 (0)