Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/modern-clubs-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@workflow/swc-plugin": patch
---

Enable custom class serialization transformations for "client" mode
40 changes: 40 additions & 0 deletions packages/swc-plugin-workflow/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,46 @@ export async function myWorkflow(data) {
myWorkflow.workflowId = "workflow//input.js//myWorkflow";
```

### Custom Serialization in Client Mode

Classes with custom serialization methods are also registered in client mode so that they can be properly serialized when passed to `start(workflow)`:

Input:
```javascript
export class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}

static [Symbol.for("workflow-serialize")](instance) {
return { x: instance.x, y: instance.y };
}

static [Symbol.for("workflow-deserialize")](data) {
return new Point(data.x, data.y);
}
}
```

Output (Client Mode):
```javascript
import { registerSerializationClass } from "workflow/internal/class-serialization";
export class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
static [Symbol.for("workflow-serialize")](instance) {
return { x: instance.x, y: instance.y };
}
static [Symbol.for("workflow-deserialize")](data) {
return new Point(data.x, data.y);
}
}
registerSerializationClass("class//input.js//Point", Point);
```

---

## Static Methods
Expand Down
122 changes: 82 additions & 40 deletions packages/swc-plugin-workflow/transform/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ mod naming;
use serde::Deserialize;
use std::collections::{HashMap, HashSet};
use swc_core::{
common::{DUMMY_SP, SyntaxContext, errors::HANDLER},
common::{errors::HANDLER, SyntaxContext, DUMMY_SP},
ecma::{
ast::*,
visit::{VisitMut, VisitMutWith, noop_visit_mut_type},
visit::{noop_visit_mut_type, VisitMut, VisitMutWith},
},
};

Expand Down Expand Up @@ -2230,6 +2230,45 @@ impl StepTransform {
}))
}

// Create a registration call statement: registerSerializationClass("class//...", ClassName)
// Used in workflow mode and client mode to register classes for serialization
fn create_class_serialization_registration(&self, class_name: &str) -> Stmt {
let class_id = naming::format_name("class", &self.filename, class_name);
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(
"registerSerializationClass".into(),
DUMMY_SP,
SyntaxContext::empty(),
)))),
args: vec![
// First argument: class ID
ExprOrSpread {
spread: None,
expr: Box::new(Expr::Lit(Lit::Str(Str {
span: DUMMY_SP,
value: class_id.into(),
raw: None,
}))),
},
// Second argument: ClassName
ExprOrSpread {
spread: None,
expr: Box::new(Expr::Ident(Ident::new(
class_name.into(),
DUMMY_SP,
SyntaxContext::empty(),
))),
},
],
type_args: None,
})),
})
}

// Create a proxy reference: globalThis[Symbol.for("WORKFLOW_USE_STEP")]("step_id", closure_fn) (workflow mode)
fn create_step_proxy_reference(&self, step_id: &str, closure_vars: &[String]) -> Expr {
let mut args = vec![ExprOrSpread {
Expand Down Expand Up @@ -3239,7 +3278,13 @@ impl VisitMut for StepTransform {
}
}
TransformMode::Client => {
// No imports needed for client mode since step functions are not transformed
// In client mode, we still need class serialization registration
// so that classes can be serialized when passed to start(workflow)
let needs_class_serialization =
!self.classes_needing_serialization.is_empty();
if needs_class_serialization {
imports_to_add.push(self.create_class_serialization_import());
}
}
}

Expand Down Expand Up @@ -3710,43 +3755,22 @@ impl VisitMut for StepTransform {
self.classes_needing_serialization.drain().collect();
sorted_classes.sort();
for class_name in sorted_classes {
// Generate class ID: class//filename//ClassName
let class_id = naming::format_name("class", &self.filename, &class_name);
let registration_call =
self.create_class_serialization_registration(&class_name);
module.body.push(ModuleItem::Stmt(registration_call));
}
}

// Create: registerSerializationClass("class//...", ClassName)
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(
"registerSerializationClass".into(),
DUMMY_SP,
SyntaxContext::empty(),
)))),
args: vec![
// First argument: class ID
ExprOrSpread {
spread: None,
expr: Box::new(Expr::Lit(Lit::Str(Str {
span: DUMMY_SP,
value: class_id.into(),
raw: None,
}))),
},
// Second argument: ClassName
ExprOrSpread {
spread: None,
expr: Box::new(Expr::Ident(Ident::new(
class_name.into(),
DUMMY_SP,
SyntaxContext::empty(),
))),
},
],
type_args: None,
})),
});
// Add class serialization registrations for client mode
// In client mode, we need classes to be registered so that serialization works
// when passing class instances to start(workflow)
if matches!(self.mode, TransformMode::Client) {
let mut sorted_classes: Vec<_> =
self.classes_needing_serialization.drain().collect();
sorted_classes.sort();
for class_name in sorted_classes {
let registration_call =
self.create_class_serialization_registration(&class_name);
module.body.push(ModuleItem::Stmt(registration_call));
}
}
Comment thread
TooTallNate marked this conversation as resolved.
Expand Down Expand Up @@ -3935,7 +3959,13 @@ impl VisitMut for StepTransform {
}
}
TransformMode::Client => {
// No imports needed for workflow mode
// In client mode, we still need class serialization registration
Comment thread
TooTallNate marked this conversation as resolved.
// so that classes can be serialized when passed to start(workflow)
let needs_class_serialization =
!self.classes_needing_serialization.is_empty();
if needs_class_serialization {
module_items.push(self.create_class_serialization_import());
}
}
}

Expand All @@ -3951,6 +3981,18 @@ impl VisitMut for StepTransform {
}
}

// Add class serialization registrations for client mode (Script case)
if matches!(self.mode, TransformMode::Client) {
let mut sorted_classes: Vec<_> =
self.classes_needing_serialization.drain().collect();
sorted_classes.sort();
for class_name in sorted_classes {
let registration_call =
self.create_class_serialization_registration(&class_name);
module_items.push(ModuleItem::Stmt(registration_call));
}
}
Comment thread
TooTallNate marked this conversation as resolved.

// Note: workflowId assignments are now handled in visit_mut_module_items

// Add metadata comment at the beginning of the module
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { registerSerializationClass } from "workflow/internal/class-serialization";
/**__internal_workflows{"steps":{"input.js":{"TestClass.staticMethod":{"stepId":"step//input.js//TestClass.staticMethod"}}}}*/;
export class TestClass {
// Error: instance methods can't have directives
Expand All @@ -15,3 +16,4 @@ export class TestClass {
return 'allowed';
}
}
registerSerializationClass("class//input.js//TestClass", TestClass);
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { registerSerializationClass } from "workflow/internal/class-serialization";
// Test custom serialization with imported symbols from '@workflow/serde'
import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde';
// Class using imported symbols
Expand Down Expand Up @@ -37,3 +38,5 @@ export class Color {
return new Color(data.r, data.g, data.b);
}
}
registerSerializationClass("class//input.js//Color", Color);
registerSerializationClass("class//input.js//Vector", Vector);
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { registerSerializationClass } from "workflow/internal/class-serialization";
// Test custom serialization with locally defined symbols using Symbol.for()
const WORKFLOW_SERIALIZE = Symbol.for('workflow-serialize');
const WORKFLOW_DESERIALIZE = Symbol.for('workflow-deserialize');
Expand Down Expand Up @@ -53,3 +54,6 @@ export class Triangle {
return new Triangle(data.a, data.b, data.c);
}
}
registerSerializationClass("class//input.js//Circle", Circle);
registerSerializationClass("class//input.js//Rectangle", Rectangle);
registerSerializationClass("class//input.js//Triangle", Triangle);
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { registerSerializationClass } from "workflow/internal/class-serialization";
// Class with custom serialization methods using symbols
export class Point {
constructor(x, y){
Expand Down Expand Up @@ -28,3 +29,4 @@ export class OnlySerialize {
};
}
}
registerSerializationClass("class//input.js//Point", Point);
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { registerSerializationClass } from "workflow/internal/class-serialization";
/**__internal_workflows{"steps":{"input.js":{"MyService.process":{"stepId":"step//input.js//MyService.process"},"MyService.transform":{"stepId":"step//input.js//MyService.transform"}}}}*/;
export class MyService {
static async process(data) {
Expand All @@ -11,3 +12,4 @@ export class MyService {
return 'regular';
}
}
registerSerializationClass("class//input.js//MyService", MyService);
Loading