diff --git a/.changeset/legal-parts-happen.md b/.changeset/legal-parts-happen.md new file mode 100644 index 0000000000..214b97c63e --- /dev/null +++ b/.changeset/legal-parts-happen.md @@ -0,0 +1,5 @@ +--- +"@workflow/swc-plugin": patch +--- + +Add support for "use step" functions in class instance methods diff --git a/packages/core/e2e/e2e.test.ts b/packages/core/e2e/e2e.test.ts index f055cbb8eb..d1be775de9 100644 --- a/packages/core/e2e/e2e.test.ts +++ b/packages/core/e2e/e2e.test.ts @@ -1416,6 +1416,62 @@ describe('e2e', () => { } ); + test( + 'instanceMethodStepWorkflow - instance methods with "use step" directive', + { timeout: 60_000 }, + async () => { + // This workflow tests instance methods marked with "use step". + // The Counter class has custom serialization so the `this` context + // (the Counter instance) can be serialized across the workflow/step boundary. + // + // instanceMethodStepWorkflow(5) should: + // 1. Create Counter(5) + // 2. counter.add(10) -> 5 + 10 = 15 + // 3. counter.multiply(3) -> 5 * 3 = 15 + // 4. counter.describe('test counter') -> { label: 'test counter', value: 5 } + // 5. Create Counter(100), call counter2.add(50) -> 100 + 50 = 150 + const run = await triggerWorkflow('instanceMethodStepWorkflow', [5]); + const returnValue = await getWorkflowReturnValue(run.runId); + + expect(returnValue).toEqual({ + initialValue: 5, + added: 15, // 5 + 10 + multiplied: 15, // 5 * 3 + description: { label: 'test counter', value: 5 }, + added2: 150, // 100 + 50 + }); + + // Verify the run completed successfully + const { json: runData } = await cliInspectJson( + `runs ${run.runId} --withData` + ); + expect(runData.status).toBe('completed'); + expect(runData.output).toEqual({ + initialValue: 5, + added: 15, + multiplied: 15, + description: { label: 'test counter', value: 5 }, + added2: 150, + }); + + // Verify the steps were executed (should have 4 steps: add, multiply, describe, add) + const { json: steps } = await cliInspectJson( + `steps --runId ${run.runId}` + ); + // Filter to only Counter instance method steps + const counterSteps = steps.filter( + (s: any) => + s.stepName.includes('Counter#add') || + s.stepName.includes('Counter#multiply') || + s.stepName.includes('Counter#describe') + ); + expect(counterSteps.length).toBe(4); // add, multiply, describe, add (from counter2) + expect(counterSteps.every((s: any) => s.status === 'completed')).toBe( + true + ); + } + ); + test( 'crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context', { timeout: 60_000 }, diff --git a/packages/swc-plugin-workflow/spec.md b/packages/swc-plugin-workflow/spec.md index ff803b1a87..11ac2da33a 100644 --- a/packages/swc-plugin-workflow/spec.md +++ b/packages/swc-plugin-workflow/spec.md @@ -42,6 +42,7 @@ Examples: - `step//src/jobs/order.ts//fetchData` - `step//src/jobs/order.ts//processOrder/innerStep` (nested step) - `step//src/jobs/order.ts//MyClass.staticMethod` (static method) +- `step//src/jobs/order.ts//MyClass#instanceMethod` (instance method) - `class//src/models/Point.ts//Point` (serialization class) --- @@ -171,6 +172,57 @@ function wrapper(multiplier) { registerStepFunction("step//input.js//wrapper/_anonymousStep0", wrapper$_anonymousStep0); ``` +### Instance Method Step + +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. + +Input: +```javascript +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@vercel/workflow'; + +export class Counter { + static [WORKFLOW_SERIALIZE](instance) { + return { value: instance.value }; + } + static [WORKFLOW_DESERIALIZE](data) { + return new Counter(data.value); + } + constructor(value) { + this.value = value; + } + async add(amount) { + 'use step'; + return this.value + amount; + } +} +``` + +Output: +```javascript +import { registerStepFunction } from "workflow/internal/private"; +import { registerSerializationClass } from "workflow/internal/class-serialization"; +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@vercel/workflow'; +/**__internal_workflows{"steps":{"input.js":{"Counter#add":{"stepId":"step//input.js//Counter#add"}}},"classes":{"input.js":{"Counter":{"classId":"class//input.js//Counter"}}}}*/; +export class Counter { + static [WORKFLOW_SERIALIZE](instance) { + return { value: instance.value }; + } + static [WORKFLOW_DESERIALIZE](data) { + return new Counter(data.value); + } + constructor(value) { + this.value = value; + } + async add(amount) { + return this.value + amount; + } +} +registerStepFunction("step//input.js//Counter#add", Counter.prototype["add"]); +registerSerializationClass("class//input.js//Counter", Counter); +``` + +Note: Instance methods use `#` in the step ID (e.g., `Counter#add`) and are registered via `ClassName.prototype["methodName"]`. + ### Module-Level Directive Input: @@ -591,7 +643,7 @@ The plugin emits errors for invalid usage: | Error | Description | |-------|-------------| | Non-async function | Functions with `"use step"` or `"use workflow"` must be async | -| Instance methods | Only static methods can have directives (not instance methods) | +| Instance methods with `"use workflow"` | Only static methods can have `"use workflow"` (not instance methods) | | Misplaced directive | Directive must be at top of file or start of function body | | Conflicting directives | Cannot have both `"use step"` and `"use workflow"` at module level | | Invalid exports | Module-level directive files can only export async functions | @@ -610,6 +662,7 @@ The plugin supports various function declaration styles: - `const name = async function() { "use step"; }` - Function expression - `{ async method() { "use step"; } }` - Object method - `static async method() { "use step"; }` - Static class method +- `async method() { "use step"; }` - Instance class method (requires custom serialization) --- diff --git a/packages/swc-plugin-workflow/transform/src/lib.rs b/packages/swc-plugin-workflow/transform/src/lib.rs index 549b153eb5..f5889ad028 100644 --- a/packages/swc-plugin-workflow/transform/src/lib.rs +++ b/packages/swc-plugin-workflow/transform/src/lib.rs @@ -368,6 +368,12 @@ pub struct StepTransform { // Track static step methods to strip from class and assign as properties (workflow mode) // (class_name, method_name, step_id) static_step_methods_to_strip: Vec<(String, String, String)>, + // Track instance method steps that need registration after the class declaration + // (class_name, method_name, step_id, span) + instance_method_step_registrations: Vec<(String, String, String, swc_core::common::Span)>, + // Track instance step methods to strip from class and assign as properties (workflow mode) + // (class_name, method_name, step_id) + instance_step_methods_to_strip: Vec<(String, String, String)>, // Track classes that need serialization registration (for `this` serialization in static methods) // Set of class names that have static step/workflow methods classes_needing_serialization: HashSet, @@ -1239,6 +1245,8 @@ impl StepTransform { static_method_step_registrations: Vec::new(), static_method_workflow_registrations: Vec::new(), static_step_methods_to_strip: Vec::new(), + instance_method_step_registrations: Vec::new(), + instance_step_methods_to_strip: Vec::new(), classes_needing_serialization: HashSet::new(), serialization_symbol_identifiers: HashMap::new(), classes_for_manifest: HashSet::new(), @@ -3514,7 +3522,8 @@ impl VisitMut for StepTransform { let needs_register_import = !self.registration_calls.is_empty() || !self.object_property_step_functions.is_empty() || !self.nested_step_functions.is_empty() - || !self.static_method_step_registrations.is_empty(); + || !self.static_method_step_registrations.is_empty() + || !self.instance_method_step_registrations.is_empty(); // Check if any nested steps have closure variables let needs_closure_import = self @@ -3876,6 +3885,65 @@ impl VisitMut for StepTransform { module.body.push(ModuleItem::Stmt(registration_call)); } + // Add instance method step registrations + // For instance methods, we register ClassName.prototype.methodName + for (class_name, method_name, step_id, _span) in + self.instance_method_step_registrations.drain(..) + { + let registration_call = Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(Expr::Call(CallExpr { + span: DUMMY_SP, + ctxt: SyntaxContext::empty(), + callee: Callee::Expr(Box::new(Expr::Ident(Ident::new( + "registerStepFunction".into(), + DUMMY_SP, + SyntaxContext::empty(), + )))), + args: vec![ + // First argument: step ID + ExprOrSpread { + spread: None, + expr: Box::new(Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: step_id.into(), + raw: None, + }))), + }, + // Second argument: ClassName.prototype.methodName + ExprOrSpread { + spread: None, + expr: Box::new(Expr::Member(MemberExpr { + span: DUMMY_SP, + obj: Box::new(Expr::Member(MemberExpr { + span: DUMMY_SP, + obj: Box::new(Expr::Ident(Ident::new( + class_name.into(), + DUMMY_SP, + SyntaxContext::empty(), + ))), + prop: MemberProp::Ident(IdentName::new( + "prototype".into(), + DUMMY_SP, + )), + })), + prop: MemberProp::Computed(ComputedPropName { + span: DUMMY_SP, + expr: Box::new(Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: method_name.into(), + raw: None, + }))), + }), + })), + }, + ], + type_args: None, + })), + }); + module.body.push(ModuleItem::Stmt(registration_call)); + } + // Add class serialization registrations for step mode // In step mode, we need: // 1. registerSerializationClass(classId, ClassName) - for deserialization @@ -4008,6 +4076,99 @@ impl VisitMut for StepTransform { module.body.push(ModuleItem::Stmt(assignment)); } + // Add instance step method property assignments (workflow mode) + // These methods were stripped from the class and need to be assigned as prototype properties + for (class_name, method_name, step_id) in + self.instance_step_methods_to_strip.drain(..) + { + // Create: ClassName.prototype.methodName = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step_id") + let proxy_expr = Expr::Call(CallExpr { + span: DUMMY_SP, + ctxt: SyntaxContext::empty(), + callee: Callee::Expr(Box::new(Expr::Member(MemberExpr { + span: DUMMY_SP, + obj: Box::new(Expr::Ident(Ident::new( + "globalThis".into(), + DUMMY_SP, + SyntaxContext::empty(), + ))), + prop: MemberProp::Computed(ComputedPropName { + span: DUMMY_SP, + expr: Box::new(Expr::Call(CallExpr { + span: DUMMY_SP, + ctxt: SyntaxContext::empty(), + callee: Callee::Expr(Box::new(Expr::Member(MemberExpr { + span: DUMMY_SP, + obj: Box::new(Expr::Ident(Ident::new( + "Symbol".into(), + DUMMY_SP, + SyntaxContext::empty(), + ))), + prop: MemberProp::Ident(IdentName::new( + "for".into(), + DUMMY_SP, + )), + }))), + args: vec![ExprOrSpread { + spread: None, + expr: Box::new(Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: "WORKFLOW_USE_STEP".into(), + raw: None, + }))), + }], + type_args: None, + })), + }), + }))), + args: vec![ExprOrSpread { + spread: None, + expr: Box::new(Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: step_id.into(), + raw: None, + }))), + }], + type_args: None, + }); + + // Create: ClassName.prototype.methodName = proxy_expr + let assignment = Stmt::Expr(ExprStmt { + span: DUMMY_SP, + expr: Box::new(Expr::Assign(AssignExpr { + span: DUMMY_SP, + left: AssignTarget::Simple(SimpleAssignTarget::Member( + MemberExpr { + span: DUMMY_SP, + obj: Box::new(Expr::Member(MemberExpr { + span: DUMMY_SP, + obj: Box::new(Expr::Ident(Ident::new( + class_name.into(), + DUMMY_SP, + SyntaxContext::empty(), + ))), + prop: MemberProp::Ident(IdentName::new( + "prototype".into(), + DUMMY_SP, + )), + })), + prop: MemberProp::Computed(ComputedPropName { + span: DUMMY_SP, + expr: Box::new(Expr::Lit(Lit::Str(Str { + span: DUMMY_SP, + value: method_name.into(), + raw: None, + }))), + }), + }, + )), + op: AssignOp::Assign, + right: Box::new(proxy_expr), + })), + }); + module.body.push(ModuleItem::Stmt(assignment)); + } + // Add class serialization registrations for workflow mode // This is now the same as step mode - using registerSerializationClass() // which sets both classId and registers in the globalThis Map @@ -6349,22 +6510,37 @@ impl VisitMut for StepTransform { // Visit the class body (this populates static_step_methods_to_strip) class_decl.class.visit_mut_with(self); - // In workflow mode, remove static step methods from the class body + // In workflow mode, remove static and instance step methods from the class body if matches!(self.mode, TransformMode::Workflow) { - let methods_to_strip: Vec<_> = self + let static_methods_to_strip: Vec<_> = self .static_step_methods_to_strip .iter() .filter(|(cn, _, _)| cn == &class_name) .map(|(_, mn, _)| mn.clone()) .collect(); - if !methods_to_strip.is_empty() { + let instance_methods_to_strip: Vec<_> = self + .instance_step_methods_to_strip + .iter() + .filter(|(cn, _, _)| cn == &class_name) + .map(|(_, mn, _)| mn.clone()) + .collect(); + + if !static_methods_to_strip.is_empty() || !instance_methods_to_strip.is_empty() { class_decl.class.body.retain(|member| { if let ClassMember::Method(method) = member { - if method.is_static { - if let PropName::Ident(ident) = &method.key { - let method_name = ident.sym.to_string(); - return !methods_to_strip.contains(&method_name); + // Handle both identifier and string keys for method names + let method_name = match &method.key { + PropName::Ident(ident) => Some(ident.sym.to_string()), + PropName::Str(s) => Some(s.value.to_string_lossy().to_string()), + _ => None, + }; + + if let Some(method_name) = method_name { + if method.is_static { + return !static_methods_to_strip.contains(&method_name); + } else { + return !instance_methods_to_strip.contains(&method_name); } } } @@ -6405,22 +6581,31 @@ impl VisitMut for StepTransform { // Visit the class body (this populates static_step_methods_to_strip) class_expr.class.visit_mut_with(self); - // In workflow mode, remove static step methods from the class body + // In workflow mode, remove static and instance step methods from the class body if matches!(self.mode, TransformMode::Workflow) { - let methods_to_strip: Vec<_> = self + let static_methods_to_strip: Vec<_> = self .static_step_methods_to_strip .iter() .filter(|(cn, _, _)| cn == &internal_class_name) .map(|(_, mn, _)| mn.clone()) .collect(); - if !methods_to_strip.is_empty() { + let instance_methods_to_strip: Vec<_> = self + .instance_step_methods_to_strip + .iter() + .filter(|(cn, _, _)| cn == &internal_class_name) + .map(|(_, mn, _)| mn.clone()) + .collect(); + + if !static_methods_to_strip.is_empty() || !instance_methods_to_strip.is_empty() { class_expr.class.body.retain(|member| { if let ClassMember::Method(method) = member { - if method.is_static { - if let PropName::Ident(ident) = &method.key { - let method_name = ident.sym.to_string(); - return !methods_to_strip.contains(&method_name); + if let PropName::Ident(ident) = &method.key { + let method_name = ident.sym.to_string(); + if method.is_static { + return !static_methods_to_strip.contains(&method_name); + } else { + return !instance_methods_to_strip.contains(&method_name); } } } @@ -6436,20 +6621,12 @@ impl VisitMut for StepTransform { // Handle class methods fn visit_mut_class_method(&mut self, method: &mut ClassMethod) { if !method.is_static { - // Instance methods can't be step/workflow functions + // Instance methods can have "use step" (but not "use workflow") let has_step = self.has_use_step_directive(&method.function.body); let has_workflow = self.has_use_workflow_directive(&method.function.body); - if has_step { - HANDLER.with(|handler| { - handler - .struct_span_err( - method.span, - "Instance methods cannot be marked with \"use step\". Only static methods, functions, and object methods are supported.", - ) - .emit() - }); - } else if has_workflow { + if has_workflow { + // Workflows on instance methods don't make sense (workflows are entry points) HANDLER.with(|handler| { handler .struct_span_err( @@ -6458,6 +6635,109 @@ impl VisitMut for StepTransform { ) .emit() }); + } else if has_step { + // Instance methods with "use step" are supported + // Validate async + if !method.function.is_async { + emit_error(WorkflowErrorKind::NonAsyncFunction { + span: method.function.span, + directive: "use step", + }); + return; + } + + // Get method name + let method_name = match &method.key { + PropName::Ident(ident) => ident.sym.to_string(), + PropName::Str(s) => s.value.to_string_lossy().to_string(), + _ => { + // Complex key - skip + method.visit_mut_children_with(self); + return; + } + }; + + // Get class name (must be set by visit_mut_class) + let class_name = match &self.current_class_name { + Some(name) => name.clone(), + None => { + // No class context - shouldn't happen, but fall back + method.visit_mut_children_with(self); + return; + } + }; + + // Generate full qualified name using # for instance methods: ClassName#methodName + let full_name = format!("{}#{}", class_name, method_name); + + // For nested step hoisting, use $ instead of # to produce valid JS identifiers + let hoisted_parent_name = format!("{}${}", class_name, method_name); + + self.step_function_names.insert(full_name.clone()); + + // Track class for serialization (needed for `this` serialization) + self.classes_needing_serialization + .insert(class_name.clone()); + + // Generate step ID + let step_id = self.create_id(Some(&full_name), method.function.span, false); + + match self.mode { + TransformMode::Step => { + // Remove directive + self.remove_use_step_directive(&mut method.function.body); + + // Track for registration after class (will use prototype) + self.instance_method_step_registrations.push(( + class_name.clone(), + method_name.clone(), + step_id, + method.function.span, + )); + + // Set current_parent_function_name for nested step hoisting + // This prevents self-referential aliases like `const helper = helper;` + // Use $ instead of # to produce valid JS identifiers + let old_parent = self.current_parent_function_name.clone(); + self.current_parent_function_name = Some(hoisted_parent_name.clone()); + + // Visit children to process nested step functions + method.visit_mut_children_with(self); + + // Restore parent function name + self.current_parent_function_name = old_parent; + } + TransformMode::Workflow => { + // Remove directive for consistency with other modes + self.remove_use_step_directive(&mut method.function.body); + + // Track this method to be stripped from the class and assigned as a property + self.instance_step_methods_to_strip.push(( + class_name.clone(), + method_name.clone(), + step_id, + )); + // Note: No need to visit children in Workflow mode since the method body + // will be stripped and replaced with a proxy call + } + TransformMode::Client => { + // Just remove directive, keep the function body + self.remove_use_step_directive(&mut method.function.body); + + // Set current_parent_function_name for nested step hoisting + // Use $ instead of # to produce valid JS identifiers + let old_parent = self.current_parent_function_name.clone(); + self.current_parent_function_name = Some(hoisted_parent_name.clone()); + + // Visit children to process nested step functions + method.visit_mut_children_with(self); + + // Restore parent function name + self.current_parent_function_name = old_parent; + } + } + } else { + method.visit_mut_children_with(self); } } else { // Static methods can be step/workflow functions @@ -6522,6 +6802,9 @@ impl VisitMut for StepTransform { step_id, method.function.span, )); + + // Visit children to process nested step functions + method.visit_mut_children_with(self); } TransformMode::Workflow => { // Remove directive for consistency with other modes @@ -6537,10 +6820,15 @@ impl VisitMut for StepTransform { method_name.clone(), step_id, )); + // Note: No need to visit children in Workflow mode since the method body + // will be stripped and replaced with a proxy call } TransformMode::Client => { // Just remove directive, keep the function body self.remove_use_step_directive(&mut method.function.body); + + // Visit children to process nested step functions + method.visit_mut_children_with(self); } } } else if has_workflow { @@ -6562,9 +6850,13 @@ impl VisitMut for StepTransform { workflow_id, method.function.span, )); + + // Visit children to process nested step functions + method.visit_mut_children_with(self); } TransformMode::Step | TransformMode::Client => { // Remove directive and replace body with error + // No need to visit children since the body is replaced self.remove_use_workflow_directive(&mut method.function.body); // Generate workflow ID diff --git a/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-step.js b/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-step.js deleted file mode 100644 index 15148cb4d9..0000000000 --- a/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-step.js +++ /dev/null @@ -1,19 +0,0 @@ -import { registerStepFunction } from "workflow/internal/private"; -/**__internal_workflows{"steps":{"input.js":{"stepWithArguments":{"stepId":"step//input.js//stepWithArguments"},"stepWithThis":{"stepId":"step//input.js//stepWithThis"}}}}*/; -export async function stepWithThis() { - // Error: this is not allowed - return this.value; -} -export async function stepWithArguments() { - // Error: arguments is not allowed - return arguments[0]; -} -class TestClass extends BaseClass { - async stepMethod() { - 'use step'; - // Error: super is not allowed - return super.method(); - } -} -registerStepFunction("step//input.js//stepWithThis", stepWithThis); -registerStepFunction("step//input.js//stepWithArguments", stepWithArguments); diff --git a/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-step.stderr b/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-step.stderr deleted file mode 100644 index b105eb0105..0000000000 --- a/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-step.stderr +++ /dev/null @@ -1,10 +0,0 @@ - x Instance methods cannot be marked with "use step". Only static methods, functions, and object methods are supported. - ,-[input.js:14:1] - 13 | class TestClass extends BaseClass { - 14 | ,-> async stepMethod() { - 15 | | 'use step'; - 16 | | // Error: super is not allowed - 17 | | return super.method(); - 18 | `-> } - 19 | } - `---- diff --git a/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-workflow.js deleted file mode 100644 index ad4ff9d306..0000000000 --- a/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-workflow.js +++ /dev/null @@ -1,10 +0,0 @@ -/**__internal_workflows{"steps":{"input.js":{"stepWithArguments":{"stepId":"step//input.js//stepWithArguments"},"stepWithThis":{"stepId":"step//input.js//stepWithThis"}}}}*/; -export var stepWithThis = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//stepWithThis"); -export var stepWithArguments = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//stepWithArguments"); -class TestClass extends BaseClass { - async stepMethod() { - 'use step'; - // Error: super is not allowed - return super.method(); - } -} diff --git a/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-workflow.stderr b/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-workflow.stderr deleted file mode 100644 index b105eb0105..0000000000 --- a/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/output-workflow.stderr +++ /dev/null @@ -1,10 +0,0 @@ - x Instance methods cannot be marked with "use step". Only static methods, functions, and object methods are supported. - ,-[input.js:14:1] - 13 | class TestClass extends BaseClass { - 14 | ,-> async stepMethod() { - 15 | | 'use step'; - 16 | | // Error: super is not allowed - 17 | | return super.method(); - 18 | `-> } - 19 | } - `---- diff --git a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/input.js b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/input.js index 8bb091ca30..02d3ebd368 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/input.js +++ b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/input.js @@ -1,17 +1,17 @@ export class TestClass { - // Error: instance methods can't have directives + // OK: instance methods can have "use step" directive async instanceMethod() { 'use step'; - return 'not allowed'; + return 'allowed'; } - // Error: instance methods can't have workflow directive either + // Error: instance methods can't have "use workflow" directive async anotherInstance() { 'use workflow'; - return 'also not allowed'; + return 'not allowed'; } - // This is ok - static methods can have directives + // OK: static methods can have directives static async staticMethod() { 'use step'; return 'allowed'; diff --git a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-client.js b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-client.js index c172cdc750..510a55b473 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-client.js +++ b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-client.js @@ -1,17 +1,16 @@ import { registerSerializationClass } from "workflow/internal/class-serialization"; -/**__internal_workflows{"steps":{"input.js":{"TestClass.staticMethod":{"stepId":"step//input.js//TestClass.staticMethod"}}},"classes":{"input.js":{"TestClass":{"classId":"class//input.js//TestClass"}}}}*/; +/**__internal_workflows{"steps":{"input.js":{"TestClass#instanceMethod":{"stepId":"step//input.js//TestClass#instanceMethod"},"TestClass.staticMethod":{"stepId":"step//input.js//TestClass.staticMethod"}}},"classes":{"input.js":{"TestClass":{"classId":"class//input.js//TestClass"}}}}*/; export class TestClass { - // Error: instance methods can't have directives + // OK: instance methods can have "use step" directive async instanceMethod() { - 'use step'; - return 'not allowed'; + return 'allowed'; } - // Error: instance methods can't have workflow directive either + // Error: instance methods can't have "use workflow" directive async anotherInstance() { 'use workflow'; - return 'also not allowed'; + return 'not allowed'; } - // This is ok - static methods can have directives + // OK: static methods can have directives static async staticMethod() { return 'allowed'; } diff --git a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-client.stderr b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-client.stderr index 815e44d29c..4f8b62f939 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-client.stderr +++ b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-client.stderr @@ -1,16 +1,8 @@ - x Instance methods cannot be marked with "use step". Only static methods, functions, and object methods are supported. - ,-[input.js:3:1] - 2 | // Error: instance methods can't have directives - 3 | ,-> async instanceMethod() { - 4 | | 'use step'; - 5 | | return 'not allowed'; - 6 | `-> } - `---- x Instance methods cannot be marked with "use workflow". Only static methods, functions, and object methods are supported. ,-[input.js:9:1] - 8 | // Error: instance methods can't have workflow directive either + 8 | // Error: instance methods can't have "use workflow" directive 9 | ,-> async anotherInstance() { 10 | | 'use workflow'; - 11 | | return 'also not allowed'; + 11 | | return 'not allowed'; 12 | `-> } `---- diff --git a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-step.js b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-step.js index 9d576d861b..d60b75811b 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-step.js +++ b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-step.js @@ -1,21 +1,21 @@ import { registerStepFunction } from "workflow/internal/private"; import { registerSerializationClass } from "workflow/internal/class-serialization"; -/**__internal_workflows{"steps":{"input.js":{"TestClass.staticMethod":{"stepId":"step//input.js//TestClass.staticMethod"}}},"classes":{"input.js":{"TestClass":{"classId":"class//input.js//TestClass"}}}}*/; +/**__internal_workflows{"steps":{"input.js":{"TestClass#instanceMethod":{"stepId":"step//input.js//TestClass#instanceMethod"},"TestClass.staticMethod":{"stepId":"step//input.js//TestClass.staticMethod"}}},"classes":{"input.js":{"TestClass":{"classId":"class//input.js//TestClass"}}}}*/; export class TestClass { - // Error: instance methods can't have directives + // OK: instance methods can have "use step" directive async instanceMethod() { - 'use step'; - return 'not allowed'; + return 'allowed'; } - // Error: instance methods can't have workflow directive either + // Error: instance methods can't have "use workflow" directive async anotherInstance() { 'use workflow'; - return 'also not allowed'; + return 'not allowed'; } - // This is ok - static methods can have directives + // OK: static methods can have directives static async staticMethod() { return 'allowed'; } } registerStepFunction("step//input.js//TestClass.staticMethod", TestClass.staticMethod); +registerStepFunction("step//input.js//TestClass#instanceMethod", TestClass.prototype["instanceMethod"]); registerSerializationClass("class//input.js//TestClass", TestClass); diff --git a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-step.stderr b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-step.stderr index 815e44d29c..4f8b62f939 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-step.stderr +++ b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-step.stderr @@ -1,16 +1,8 @@ - x Instance methods cannot be marked with "use step". Only static methods, functions, and object methods are supported. - ,-[input.js:3:1] - 2 | // Error: instance methods can't have directives - 3 | ,-> async instanceMethod() { - 4 | | 'use step'; - 5 | | return 'not allowed'; - 6 | `-> } - `---- x Instance methods cannot be marked with "use workflow". Only static methods, functions, and object methods are supported. ,-[input.js:9:1] - 8 | // Error: instance methods can't have workflow directive either + 8 | // Error: instance methods can't have "use workflow" directive 9 | ,-> async anotherInstance() { 10 | | 'use workflow'; - 11 | | return 'also not allowed'; + 11 | | return 'not allowed'; 12 | `-> } `---- diff --git a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-workflow.js index 1ea89bfe3b..4cc2445caa 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-workflow.js +++ b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-workflow.js @@ -1,16 +1,12 @@ import { registerSerializationClass } from "workflow/internal/class-serialization"; -/**__internal_workflows{"steps":{"input.js":{"TestClass.staticMethod":{"stepId":"step//input.js//TestClass.staticMethod"}}},"classes":{"input.js":{"TestClass":{"classId":"class//input.js//TestClass"}}}}*/; +/**__internal_workflows{"steps":{"input.js":{"TestClass#instanceMethod":{"stepId":"step//input.js//TestClass#instanceMethod"},"TestClass.staticMethod":{"stepId":"step//input.js//TestClass.staticMethod"}}},"classes":{"input.js":{"TestClass":{"classId":"class//input.js//TestClass"}}}}*/; export class TestClass { - // Error: instance methods can't have directives - async instanceMethod() { - 'use step'; - return 'not allowed'; - } - // Error: instance methods can't have workflow directive either + // Error: instance methods can't have "use workflow" directive async anotherInstance() { 'use workflow'; - return 'also not allowed'; + return 'not allowed'; } } TestClass.staticMethod = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//TestClass.staticMethod"); +TestClass.prototype["instanceMethod"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//TestClass#instanceMethod"); registerSerializationClass("class//input.js//TestClass", TestClass); diff --git a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-workflow.stderr b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-workflow.stderr index 815e44d29c..4f8b62f939 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-workflow.stderr +++ b/packages/swc-plugin-workflow/transform/tests/errors/instance-methods/output-workflow.stderr @@ -1,16 +1,8 @@ - x Instance methods cannot be marked with "use step". Only static methods, functions, and object methods are supported. - ,-[input.js:3:1] - 2 | // Error: instance methods can't have directives - 3 | ,-> async instanceMethod() { - 4 | | 'use step'; - 5 | | return 'not allowed'; - 6 | `-> } - `---- x Instance methods cannot be marked with "use workflow". Only static methods, functions, and object methods are supported. ,-[input.js:9:1] - 8 | // Error: instance methods can't have workflow directive either + 8 | // Error: instance methods can't have "use workflow" directive 9 | ,-> async anotherInstance() { 10 | | 'use workflow'; - 11 | | return 'also not allowed'; + 11 | | return 'not allowed'; 12 | `-> } `---- diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/input.js b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/input.js new file mode 100644 index 0000000000..180d4cea4a --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/input.js @@ -0,0 +1,29 @@ +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@vercel/workflow'; + +export class Service { + static [WORKFLOW_SERIALIZE](instance) { + return { value: instance.value }; + } + + static [WORKFLOW_DESERIALIZE](data) { + return new Service(data.value); + } + + constructor(value) { + this.value = value; + } + + // Instance method step that contains a nested step function + async process(input) { + 'use step'; + + // This nested step should be transformed + const helper = async (x) => { + 'use step'; + return x * 2; + }; + + const doubled = await helper(input); + return doubled + this.value; + } +} diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-client.js b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-client.js new file mode 100644 index 0000000000..6aec6db9df --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-client.js @@ -0,0 +1,26 @@ +import { registerSerializationClass } from "workflow/internal/class-serialization"; +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@vercel/workflow'; +/**__internal_workflows{"steps":{"input.js":{"Service#process":{"stepId":"step//input.js//Service#process"},"helper":{"stepId":"step//input.js//helper"}}},"classes":{"input.js":{"Service":{"classId":"class//input.js//Service"}}}}*/; +export class Service { + static [WORKFLOW_SERIALIZE](instance) { + return { + value: instance.value + }; + } + static [WORKFLOW_DESERIALIZE](data) { + return new Service(data.value); + } + constructor(value){ + this.value = value; + } + // Instance method step that contains a nested step function + async process(input) { + // This nested step should be transformed + const helper = async (x)=>{ + return x * 2; + }; + const doubled = await helper(input); + return doubled + this.value; + } +} +registerSerializationClass("class//input.js//Service", Service); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-step.js b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-step.js new file mode 100644 index 0000000000..cf03d037b7 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-step.js @@ -0,0 +1,28 @@ +import { registerStepFunction } from "workflow/internal/private"; +import { registerSerializationClass } from "workflow/internal/class-serialization"; +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@vercel/workflow'; +/**__internal_workflows{"steps":{"input.js":{"Service#process":{"stepId":"step//input.js//Service#process"},"helper":{"stepId":"step//input.js//helper"}}},"classes":{"input.js":{"Service":{"classId":"class//input.js//Service"}}}}*/; +var Service$process$helper = async (x)=>x * 2; +export class Service { + static [WORKFLOW_SERIALIZE](instance) { + return { + value: instance.value + }; + } + static [WORKFLOW_DESERIALIZE](data) { + return new Service(data.value); + } + constructor(value){ + this.value = value; + } + // Instance method step that contains a nested step function + async process(input) { + // This nested step should be transformed + const helper = Service$process$helper; + const doubled = await helper(input); + return doubled + this.value; + } +} +registerStepFunction("step//input.js//Service$process/helper", Service$process$helper); +registerStepFunction("step//input.js//Service#process", Service.prototype["process"]); +registerSerializationClass("class//input.js//Service", Service); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-workflow.js new file mode 100644 index 0000000000..dd8ef1973f --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-nested-step/output-workflow.js @@ -0,0 +1,18 @@ +import { registerSerializationClass } from "workflow/internal/class-serialization"; +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@vercel/workflow'; +/**__internal_workflows{"steps":{"input.js":{"Service#process":{"stepId":"step//input.js//Service#process"}}},"classes":{"input.js":{"Service":{"classId":"class//input.js//Service"}}}}*/; +export class Service { + static [WORKFLOW_SERIALIZE](instance) { + return { + value: instance.value + }; + } + static [WORKFLOW_DESERIALIZE](data) { + return new Service(data.value); + } + constructor(value){ + this.value = value; + } +} +Service.prototype["process"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//Service#process"); +registerSerializationClass("class//input.js//Service", Service); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/input.js b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/input.js new file mode 100644 index 0000000000..88af31bf34 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/input.js @@ -0,0 +1,25 @@ +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@vercel/workflow'; + +export class Calculator { + static [WORKFLOW_SERIALIZE](instance) { + return { multiplier: instance.multiplier }; + } + + static [WORKFLOW_DESERIALIZE](data) { + return new Calculator(data.multiplier); + } + + constructor(multiplier) { + this.multiplier = multiplier; + } + + async multiply(value) { + 'use step'; + return value * this.multiplier; + } + + async add(a, b) { + 'use step'; + return a + b + this.multiplier; + } +} diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-client.js b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-client.js new file mode 100644 index 0000000000..dc0c021ef5 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-client.js @@ -0,0 +1,23 @@ +import { registerSerializationClass } from "workflow/internal/class-serialization"; +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@vercel/workflow'; +/**__internal_workflows{"steps":{"input.js":{"Calculator#add":{"stepId":"step//input.js//Calculator#add"},"Calculator#multiply":{"stepId":"step//input.js//Calculator#multiply"}}},"classes":{"input.js":{"Calculator":{"classId":"class//input.js//Calculator"}}}}*/; +export class Calculator { + static [WORKFLOW_SERIALIZE](instance) { + return { + multiplier: instance.multiplier + }; + } + static [WORKFLOW_DESERIALIZE](data) { + return new Calculator(data.multiplier); + } + constructor(multiplier){ + this.multiplier = multiplier; + } + async multiply(value) { + return value * this.multiplier; + } + async add(a, b) { + return a + b + this.multiplier; + } +} +registerSerializationClass("class//input.js//Calculator", Calculator); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-step.js b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-step.js new file mode 100644 index 0000000000..242354e374 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-step.js @@ -0,0 +1,26 @@ +import { registerStepFunction } from "workflow/internal/private"; +import { registerSerializationClass } from "workflow/internal/class-serialization"; +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@vercel/workflow'; +/**__internal_workflows{"steps":{"input.js":{"Calculator#add":{"stepId":"step//input.js//Calculator#add"},"Calculator#multiply":{"stepId":"step//input.js//Calculator#multiply"}}},"classes":{"input.js":{"Calculator":{"classId":"class//input.js//Calculator"}}}}*/; +export class Calculator { + static [WORKFLOW_SERIALIZE](instance) { + return { + multiplier: instance.multiplier + }; + } + static [WORKFLOW_DESERIALIZE](data) { + return new Calculator(data.multiplier); + } + constructor(multiplier){ + this.multiplier = multiplier; + } + async multiply(value) { + return value * this.multiplier; + } + async add(a, b) { + return a + b + this.multiplier; + } +} +registerStepFunction("step//input.js//Calculator#multiply", Calculator.prototype["multiply"]); +registerStepFunction("step//input.js//Calculator#add", Calculator.prototype["add"]); +registerSerializationClass("class//input.js//Calculator", Calculator); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-workflow.js new file mode 100644 index 0000000000..bd6d5c91ba --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/instance-method-step/output-workflow.js @@ -0,0 +1,19 @@ +import { registerSerializationClass } from "workflow/internal/class-serialization"; +import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@vercel/workflow'; +/**__internal_workflows{"steps":{"input.js":{"Calculator#add":{"stepId":"step//input.js//Calculator#add"},"Calculator#multiply":{"stepId":"step//input.js//Calculator#multiply"}}},"classes":{"input.js":{"Calculator":{"classId":"class//input.js//Calculator"}}}}*/; +export class Calculator { + static [WORKFLOW_SERIALIZE](instance) { + return { + multiplier: instance.multiplier + }; + } + static [WORKFLOW_DESERIALIZE](data) { + return new Calculator(data.multiplier); + } + constructor(multiplier){ + this.multiplier = multiplier; + } +} +Calculator.prototype["multiply"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//Calculator#multiply"); +Calculator.prototype["add"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//Calculator#add"); +registerSerializationClass("class//input.js//Calculator", Calculator); diff --git a/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/input.js b/packages/swc-plugin-workflow/transform/tests/fixture/step-with-this-arguments-super/input.js similarity index 67% rename from packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/input.js rename to packages/swc-plugin-workflow/transform/tests/fixture/step-with-this-arguments-super/input.js index 7a8afca5a0..aea032cd64 100644 --- a/packages/swc-plugin-workflow/transform/tests/errors/forbidden-expressions/input.js +++ b/packages/swc-plugin-workflow/transform/tests/fixture/step-with-this-arguments-super/input.js @@ -1,19 +1,19 @@ export async function stepWithThis() { 'use step'; - // Error: this is not allowed + // `this` is allowed in step functions return this.value; } export async function stepWithArguments() { 'use step'; - // Error: arguments is not allowed + // `arguments` is allowed in step functions return arguments[0]; } class TestClass extends BaseClass { async stepMethod() { 'use step'; - // Error: super is not allowed + // `super` is allowed in step functions return super.method(); } } diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/step-with-this-arguments-super/output-client.js b/packages/swc-plugin-workflow/transform/tests/fixture/step-with-this-arguments-super/output-client.js new file mode 100644 index 0000000000..9e9ff263f9 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/step-with-this-arguments-super/output-client.js @@ -0,0 +1,17 @@ +import { registerSerializationClass } from "workflow/internal/class-serialization"; +/**__internal_workflows{"steps":{"input.js":{"TestClass#stepMethod":{"stepId":"step//input.js//TestClass#stepMethod"},"stepWithArguments":{"stepId":"step//input.js//stepWithArguments"},"stepWithThis":{"stepId":"step//input.js//stepWithThis"}}},"classes":{"input.js":{"TestClass":{"classId":"class//input.js//TestClass"}}}}*/; +export async function stepWithThis() { + // `this` is allowed in step functions + return this.value; +} +export async function stepWithArguments() { + // `arguments` is allowed in step functions + return arguments[0]; +} +class TestClass extends BaseClass { + async stepMethod() { + // `super` is allowed in step functions + return super.method(); + } +} +registerSerializationClass("class//input.js//TestClass", TestClass); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/step-with-this-arguments-super/output-step.js b/packages/swc-plugin-workflow/transform/tests/fixture/step-with-this-arguments-super/output-step.js new file mode 100644 index 0000000000..382c64e772 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/step-with-this-arguments-super/output-step.js @@ -0,0 +1,21 @@ +import { registerStepFunction } from "workflow/internal/private"; +import { registerSerializationClass } from "workflow/internal/class-serialization"; +/**__internal_workflows{"steps":{"input.js":{"TestClass#stepMethod":{"stepId":"step//input.js//TestClass#stepMethod"},"stepWithArguments":{"stepId":"step//input.js//stepWithArguments"},"stepWithThis":{"stepId":"step//input.js//stepWithThis"}}},"classes":{"input.js":{"TestClass":{"classId":"class//input.js//TestClass"}}}}*/; +export async function stepWithThis() { + // `this` is allowed in step functions + return this.value; +} +export async function stepWithArguments() { + // `arguments` is allowed in step functions + return arguments[0]; +} +class TestClass extends BaseClass { + async stepMethod() { + // `super` is allowed in step functions + return super.method(); + } +} +registerStepFunction("step//input.js//stepWithThis", stepWithThis); +registerStepFunction("step//input.js//stepWithArguments", stepWithArguments); +registerStepFunction("step//input.js//TestClass#stepMethod", TestClass.prototype["stepMethod"]); +registerSerializationClass("class//input.js//TestClass", TestClass); diff --git a/packages/swc-plugin-workflow/transform/tests/fixture/step-with-this-arguments-super/output-workflow.js b/packages/swc-plugin-workflow/transform/tests/fixture/step-with-this-arguments-super/output-workflow.js new file mode 100644 index 0000000000..53ad0110e6 --- /dev/null +++ b/packages/swc-plugin-workflow/transform/tests/fixture/step-with-this-arguments-super/output-workflow.js @@ -0,0 +1,8 @@ +import { registerSerializationClass } from "workflow/internal/class-serialization"; +/**__internal_workflows{"steps":{"input.js":{"TestClass#stepMethod":{"stepId":"step//input.js//TestClass#stepMethod"},"stepWithArguments":{"stepId":"step//input.js//stepWithArguments"},"stepWithThis":{"stepId":"step//input.js//stepWithThis"}}},"classes":{"input.js":{"TestClass":{"classId":"class//input.js//TestClass"}}}}*/; +export var stepWithThis = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//stepWithThis"); +export var stepWithArguments = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//stepWithArguments"); +class TestClass extends BaseClass { +} +TestClass.prototype["stepMethod"] = globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step//input.js//TestClass#stepMethod"); +registerSerializationClass("class//input.js//TestClass", TestClass); diff --git a/workbench/example/workflows/99_e2e.ts b/workbench/example/workflows/99_e2e.ts index c367072d46..0c2f362406 100644 --- a/workbench/example/workflows/99_e2e.ts +++ b/workbench/example/workflows/99_e2e.ts @@ -1147,3 +1147,84 @@ export async function crossContextSerdeWorkflow() { arraySum: { x: arraySum.x, y: arraySum.y, z: arraySum.z }, }; } + +////////////////////////////////////////////////////////// +// Instance Method Step Tests +////////////////////////////////////////////////////////// + +/** + * A class with instance methods that are marked as steps. + * This tests the new "use step" support for instance methods. + * The class uses custom serialization so the `this` value can be + * serialized across the workflow/step boundary. + */ +export class Counter { + constructor(public value: number) {} + + /** Custom serialization - converts instance to plain object */ + static [Symbol.for('workflow-serialize')](instance: Counter) { + return { value: instance.value }; + } + + /** Custom deserialization - reconstructs instance from plain object */ + static [Symbol.for('workflow-deserialize')](data: { value: number }) { + return new Counter(data.value); + } + + /** + * Instance method step: returns the sum of the counter's value and the given amount. + * The `this` context (the Counter instance) is serialized and passed + * to the step handler, then deserialized before the method is called. + */ + async add(amount: number): Promise { + 'use step'; + return this.value + amount; + } + + /** + * Instance method step: multiplies the counter's value by the given factor. + */ + async multiply(factor: number): Promise { + 'use step'; + return this.value * factor; + } + + /** + * Instance method step: returns an object with both the original and computed values. + * This tests that `this` is correctly preserved through the step execution. + */ + async describe(label: string): Promise<{ label: string; value: number }> { + 'use step'; + return { label, value: this.value }; + } +} + +/** + * Workflow that tests instance method steps. + * Creates Counter instances and calls their instance methods as steps. + * The `this` context (the Counter instance) should be serialized and + * correctly restored when the step executes. + */ +export async function instanceMethodStepWorkflow(initialValue: number) { + 'use workflow'; + + // Create a Counter instance + const counter = new Counter(initialValue); + + // Call instance method steps + const added = await counter.add(10); + const multiplied = await counter.multiply(3); + const description = await counter.describe('test counter'); + + // Create another counter to verify different instances work + const counter2 = new Counter(100); + const added2 = await counter2.add(50); + + return { + initialValue, + added, // initialValue + 10 + multiplied, // initialValue * 3 + description, // { label: 'test counter', value: initialValue } + added2, // 100 + 50 = 150 + }; +}