|
2 | 2 |
|
3 | 3 | The `"use step"` and `"use workflow"` directives work similarly to `"use server"` in React. A function marked with `"use step"` represents a durable step that executes on the server. A function marked with `"use workflow"` represents a durable workflow that orchestrates steps. |
4 | 4 |
|
5 | | -The SWC plugin has 4 modes: **Step mode**, **Workflow mode**, **Client mode**, and **Detect mode**. |
| 5 | +The SWC plugin has 3 modes: **Step mode**, **Workflow mode**, and **Detect mode**. |
6 | 6 |
|
7 | 7 | ## Directive Placement |
8 | 8 |
|
@@ -91,6 +91,10 @@ Note: File extensions are stripped from local paths for cleaner IDs. |
91 | 91 |
|
92 | 92 | In step mode, step function bodies are kept intact and registered using an inline IIFE that stores them in a global registry via `Symbol.for("@workflow/core//registeredSteps")`, with no module imports. Workflow functions throw an error if called directly (since they should only run in the workflow runtime). |
93 | 93 |
|
| 94 | +After the step-mode rewrite, the transform also runs a dead code elimination (DCE) pass. Because step bodies are preserved (unlike workflow mode where they are replaced with proxies), imports, helper functions, and other declarations referenced from step bodies are also preserved. However, code that is reachable only from workflow bodies that were replaced with throwing stubs can still be removed. |
| 95 | + |
| 96 | +Object property step functions are hoisted to module-level variables and the original call site is replaced with a reference to the hoisted variable, making `.stepId` accessible at the call site. |
| 97 | + |
94 | 98 | ### Basic Step Function |
95 | 99 |
|
96 | 100 | Input: |
@@ -249,24 +253,7 @@ export const vade = agent({ |
249 | 253 | }); |
250 | 254 | ``` |
251 | 255 |
|
252 | | -Output (Client Mode): |
253 | | -```javascript |
254 | | -import { agent } from "experimental-agent"; |
255 | | -/**__internal_workflows{"steps":{"input.js":{"vade/tools/VercelRequest/execute":{"stepId":"step//./input//vade/tools/VercelRequest/execute"}}}}*/; |
256 | | -var vade$tools$VercelRequest$execute = async function(input, ctx) { |
257 | | - return 1 + 1; |
258 | | -}; |
259 | | -export const vade = agent({ |
260 | | - tools: { |
261 | | - VercelRequest: { |
262 | | - execute: vade$tools$VercelRequest$execute |
263 | | - } |
264 | | - } |
265 | | -}); |
266 | | -vade$tools$VercelRequest$execute.stepId = "step//./input//vade/tools/VercelRequest/execute"; |
267 | | -``` |
268 | | - |
269 | | -Note: In client mode, nested object property step functions are hoisted and have `stepId` set directly via inline property assignment. Step mode also uses inline registration (no import) via a self-contained IIFE. The original call site is replaced with a reference to the hoisted variable in both modes. |
| 256 | +Note: In step mode, nested object property step functions are hoisted and registered via a self-contained IIFE (no imports). The original call site is replaced with a reference to the hoisted variable. |
270 | 257 |
|
271 | 258 | Note: The step ID includes the full path through nested objects (`vade/tools/VercelRequest/execute`), while the hoisted variable name uses `$` as the separator (`vade$tools$VercelRequest$execute`) to create a valid JavaScript identifier. |
272 | 259 |
|
@@ -451,7 +438,7 @@ export async function subtract(a, b) { |
451 | 438 |
|
452 | 439 | In workflow mode, step function bodies are replaced with a `globalThis[Symbol.for("WORKFLOW_USE_STEP")]` call. Workflow functions keep their bodies and are registered with `globalThis.__private_workflows.set()`. |
453 | 440 |
|
454 | | -After the workflow-mode rewrite, the transform also runs a dead code elimination (DCE) pass. This pruning only affects the emitted workflow/client outputs, not step-mode output. In workflow mode, because step bodies are replaced with step proxies, imports, helper functions, nested steps, and other pure statements that were only referenced from those original step bodies become eligible for removal. Exports and any identifiers still referenced by the transformed workflow code are preserved. |
| 441 | +After the workflow-mode rewrite, the transform also runs a dead code elimination (DCE) pass. Because step bodies are replaced with step proxies, imports, helper functions, nested steps, and other pure statements that were only referenced from those original step bodies become eligible for removal. Exports and any identifiers still referenced by the transformed workflow code are preserved. |
455 | 442 |
|
456 | 443 | ### Step Functions |
457 | 444 |
|
@@ -526,98 +513,6 @@ globalThis.__private_workflows.set("workflow//./input//myWorkflow", myWorkflow); |
526 | 513 |
|
527 | 514 | --- |
528 | 515 |
|
529 | | -## Client Mode |
530 | | - |
531 | | -In client mode, step function bodies are preserved as-is (allowing local testing/execution), and step functions have their `stepId` property set so they can be properly serialized when passed across boundaries (e.g., as arguments to `start()` or returned from other step functions). Workflow functions throw an error and have `workflowId` attached for use with `start()`. |
532 | | - |
533 | | -Like step mode, client mode also uses inline property assignments with no imports. The `stepId` property is set directly on the function, similar to how `workflowId` is set on workflow functions. The difference is that client mode uses a simple property assignment, while step mode uses an inline IIFE that also adds the function to the global step registry. |
534 | | - |
535 | | -Client mode also runs the same DCE pass after transform. The key difference from workflow mode is that module-level step bodies are still preserved and executable, so any imports, local helpers, or other declarations that are referenced only from those step bodies must also be preserved. By contrast, code that is reachable only from workflow bodies that were replaced with throwing stubs can still be removed. |
536 | | - |
537 | | -Note: Step functions nested inside other functions (whether workflow functions or regular functions) do NOT get `stepId` assignments in client mode because they are not accessible at module level. In practice, nested steps and helpers that are only reachable from a workflow body are often pruned by the client-mode DCE pass once that workflow body has been replaced. |
538 | | - |
539 | | -### Step Functions |
540 | | - |
541 | | -Input: |
542 | | -```javascript |
543 | | -export async function add(a, b) { |
544 | | - "use step"; |
545 | | - return a + b; |
546 | | -} |
547 | | -``` |
548 | | - |
549 | | -Output: |
550 | | -```javascript |
551 | | -/**__internal_workflows{"steps":{"input.js":{"add":{"stepId":"step//./input//add"}}}}*/; |
552 | | -export async function add(a, b) { |
553 | | - return a + b; |
554 | | -} |
555 | | -add.stepId = "step//./input//add"; |
556 | | -``` |
557 | | - |
558 | | -### Workflow Functions |
559 | | - |
560 | | -Input: |
561 | | -```javascript |
562 | | -export async function myWorkflow(data) { |
563 | | - "use workflow"; |
564 | | - return await processData(data); |
565 | | -} |
566 | | -``` |
567 | | - |
568 | | -Output: |
569 | | -```javascript |
570 | | -/**__internal_workflows{"workflows":{"input.js":{"myWorkflow":{"workflowId":"workflow//./input//myWorkflow"}}}}*/; |
571 | | -export async function myWorkflow(data) { |
572 | | - throw new Error("You attempted to execute workflow myWorkflow function directly. To start a workflow, use start(myWorkflow) from workflow/api"); |
573 | | -} |
574 | | -myWorkflow.workflowId = "workflow//./input//myWorkflow"; |
575 | | -``` |
576 | | - |
577 | | -### Custom Serialization in Client Mode |
578 | | - |
579 | | -Classes with custom serialization methods are also registered in client mode so that they can be properly serialized when passed to `start(workflow)`: |
580 | | - |
581 | | -Input: |
582 | | -```javascript |
583 | | -export class Point { |
584 | | - constructor(x, y) { |
585 | | - this.x = x; |
586 | | - this.y = y; |
587 | | - } |
588 | | - |
589 | | - static [Symbol.for("workflow-serialize")](instance) { |
590 | | - return { x: instance.x, y: instance.y }; |
591 | | - } |
592 | | - |
593 | | - static [Symbol.for("workflow-deserialize")](data) { |
594 | | - return new Point(data.x, data.y); |
595 | | - } |
596 | | -} |
597 | | -``` |
598 | | - |
599 | | -Output (Client Mode): |
600 | | -```javascript |
601 | | -/**__internal_workflows{"classes":{"input.js":{"Point":{"classId":"class//./input//Point"}}}}*/; |
602 | | -export class Point { |
603 | | - constructor(x, y) { |
604 | | - this.x = x; |
605 | | - this.y = y; |
606 | | - } |
607 | | - static [Symbol.for("workflow-serialize")](instance) { |
608 | | - return { x: instance.x, y: instance.y }; |
609 | | - } |
610 | | - static [Symbol.for("workflow-deserialize")](data) { |
611 | | - return new Point(data.x, data.y); |
612 | | - } |
613 | | -} |
614 | | -(function(__wf_cls, __wf_id) { |
615 | | - var __wf_sym = Symbol.for("workflow-class-registry"), __wf_reg = globalThis[__wf_sym] || (globalThis[__wf_sym] = new Map()); |
616 | | - __wf_reg.set(__wf_id, __wf_cls); |
617 | | - Object.defineProperty(__wf_cls, "classId", { value: __wf_id, writable: false, enumerable: false, configurable: false }); |
618 | | -})(Point, "class//./input//Point"); |
619 | | -``` |
620 | | - |
621 | 516 | --- |
622 | 517 |
|
623 | 518 | ## Detect Mode |
@@ -1009,21 +904,18 @@ This allows serialization classes to be defined in separate files (such as Next. |
1009 | 904 |
|
1010 | 905 | ### Cross-Context Class Registration |
1011 | 906 |
|
1012 | | -Classes with custom serialization are automatically included in **all bundle contexts** (step, workflow, client) to ensure they can be properly serialized and deserialized when crossing execution boundaries: |
| 907 | +Classes with custom serialization are automatically included in **all bundle contexts** (step and workflow) to ensure they can be properly serialized and deserialized when crossing execution boundaries: |
1013 | 908 |
|
1014 | 909 | | Boundary | Serializer | Deserializer | Example | |
1015 | 910 | |----------|------------|--------------|---------| |
1016 | | -| Client → Workflow | Client mode | Workflow mode | Passing a `Point` instance to `start(workflow)` | |
1017 | 911 | | Workflow → Step | Workflow mode | Step mode | Passing a `Point` instance as step argument | |
1018 | 912 | | Step → Workflow | Step mode | Workflow mode | Returning a `Point` instance from a step | |
1019 | | -| Workflow → Client | Workflow mode | Client mode | Returning a `Point` instance from a workflow | |
1020 | 913 |
|
1021 | 914 | The build system automatically discovers all files containing serializable classes and includes them in each bundle, regardless of where the class is originally defined. This ensures the class registry has all necessary classes for any serialization boundary the data may cross. |
1022 | 915 |
|
1023 | 916 | For example, if a class `Point` is defined in `models/point.ts` and only used in step code: |
1024 | 917 | - The **step bundle** includes `Point` because the step file imports it |
1025 | 918 | - The **workflow bundle** also includes `Point` so it can deserialize step return values |
1026 | | -- The **client bundle** also includes `Point` so it can deserialize workflow return values |
1027 | 919 |
|
1028 | 920 | This cross-registration happens automatically during the build process - no manual configuration is required. |
1029 | 921 |
|
@@ -1124,8 +1016,6 @@ Object.defineProperty(ClassName.prototype, "prop", { |
1124 | 1016 | }); |
1125 | 1017 | ``` |
1126 | 1018 |
|
1127 | | -**Client mode**: The getter is preserved with the directive stripped (no registration). |
1128 | | - |
1129 | 1019 | ### Static getter transformation |
1130 | 1020 |
|
1131 | 1021 | Same as instance getters but targets `ClassName` instead of `ClassName.prototype`, and uses `.` separator in the step ID (same as static methods). |
@@ -1162,8 +1052,6 @@ const obj = { |
1162 | 1052 | }; |
1163 | 1053 | ``` |
1164 | 1054 |
|
1165 | | -**Client mode**: Same as step mode — the getter body is hoisted for `stepId` assignment, original getter preserved. |
1166 | | - |
1167 | 1055 | ### Private member dead code elimination |
1168 | 1056 |
|
1169 | 1057 | In workflow mode, after stripping `"use step"` methods and getters from a class body, the plugin eliminates private class members that are no longer referenced by any remaining (non-private) member. This applies to both: |
|
0 commit comments