Add .rollback() api for workflow step promises#6525
Add .rollback() api for workflow step promises#6525LuisDuarte1 wants to merge 12 commits intomainfrom
Conversation
There was a problem hiding this comment.
I'm Bonk, and I've done a quick review of your PR.
This PR adds a .rollback() API to workflow step promises, gated behind the workflows_step_rollback compat flag. step.do() and step.waitForEvent() now return a StepPromise that supports registering a compensation function before the RPC fires.
Issues found (highest severity first):
- [HIGH] The type declarations in
rpc.d.tsand generated snapshots changeWorkflowStep.do()andwaitForEvent()return types fromPromise<T>toStepPromise<T>unconditionally, but the runtime behavior is gated behind a compat flag. When the flag is off, the raw RPC stub is used and returns a plainPromisewith no.rollback()method. This is a type-level backward compatibility break for users who haven't opted in. - [MEDIUM]
RollbackContext.outputtype is inconsistent across snapshot files:NonNullable<T> | undefinedin.d.tsfiles vsT | undefinedin.tsfiles. These should match. - [LOW]
StepPromiseoverridesthen()but does not overridecatch(). Whilecatch()delegates throughthen()per ES spec for native promises, subclasses that overridethen()should also overridecatch()explicitly to be safe against runtimes or tooling that resolvecatchindependently. - [LOW] Missing
$compatDisableFlagon the new compat flag. Other flags that will eventually become defaults include a disable flag so workers can opt out after the enable date. If this is intentional (experimental, no default date yet), no action needed — but worth confirming.
This review was generated by an AI assistant and may contain errors. Please verify all suggestions before applying.
types/defines/rpc.d.ts
Outdated
| name: string, | ||
| callback: (ctx: WorkflowStepContext) => Promise<T> | ||
| ): Promise<T>; | ||
| ): StepPromise<T>; |
There was a problem hiding this comment.
[HIGH] Backward compatibility: The return type of WorkflowStep.do() is changed from Promise<T> to StepPromise<T> unconditionally, but at runtime this only holds when the workflows_step_rollback compat flag is enabled. Without the flag, the raw RPC stub returns a plain Promise with no .rollback() method.
Users on older compat dates (flag off) will see StepPromise<T> in their editor, call .rollback(), and get a runtime error.
Consider keeping the return type as Promise<T> here and only advertising StepPromise<T> when the flag is known to be enabled (or document that .rollback() requires the flag).
| }; | ||
| export interface RollbackContext<T> { | ||
| error: Error; | ||
| output: T | undefined; |
There was a problem hiding this comment.
[MEDIUM] Inconsistent type: this file defines output: T | undefined, but the corresponding .d.ts files (rpc.d.ts, experimental/index.d.ts, latest/index.d.ts) define output: NonNullable<T> | undefined. These should be consistent — either all NonNullable<T> | undefined or all T | undefined.
| output: T | undefined; | |
| output: NonNullable<T> | undefined; |
| }; | ||
| export interface RollbackContext<T> { | ||
| error: Error; | ||
| output: T | undefined; |
There was a problem hiding this comment.
[MEDIUM] Same inconsistency as the experimental .ts snapshot — T | undefined here vs NonNullable<T> | undefined in the .d.ts files.
| output: T | undefined; | |
| output: NonNullable<T> | undefined; |
| override then<TResult1 = unknown, TResult2 = never>( | ||
| onFulfilled?: ((value: unknown) => TResult1 | PromiseLike<TResult1>) | null, | ||
| onRejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null | ||
| ): Promise<TResult1 | TResult2> { | ||
| return this.#getPromise().then(onFulfilled, onRejected); | ||
| } |
There was a problem hiding this comment.
[LOW] StepPromise overrides then() but relies on the ES spec for catch() delegation. For defensive correctness, consider also overriding catch() explicitly:
| override then<TResult1 = unknown, TResult2 = never>( | |
| onFulfilled?: ((value: unknown) => TResult1 | PromiseLike<TResult1>) | null, | |
| onRejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null | |
| ): Promise<TResult1 | TResult2> { | |
| return this.#getPromise().then(onFulfilled, onRejected); | |
| } | |
| override then<TResult1 = unknown, TResult2 = never>( | |
| onFulfilled?: ((value: unknown) => TResult1 | PromiseLike<TResult1>) | null, | |
| onRejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null | |
| ): Promise<TResult1 | TResult2> { | |
| return this.#getPromise().then(onFulfilled, onRejected); | |
| } | |
| override catch<TResult = never>( | |
| onRejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null | |
| ): Promise<unknown | TResult> { | |
| return this.#getPromise().catch(onRejected); | |
| } |
|
|
||
| workflowsStepRollback @174 :Bool | ||
| $compatEnableFlag("workflows_step_rollback") | ||
| $experimental; |
There was a problem hiding this comment.
[LOW] No $compatDisableFlag is defined. If this flag will eventually get an enable date and become default, a disable flag should be added now so workers can opt out. If this is intentional because it's experimental with no planned default date, consider adding a comment noting that.
|
Review posted on PR #6525. Here's a summary of the findings:
|
|
The generated output of Full Type Diffdiff -r types/generated-snapshot/experimental/index.d.ts bazel-bin/types/definitions/experimental/index.d.ts
3940,3941d3939
< readonly __stdoutp: ArrayBuffer;
< readonly __stderrp: ArrayBuffer;
3947,3949c3945,3947
< __stdinp?: ReadableStream | "pipe";
< __stdoutp?: "pipe" | "ignore";
< __stderrp?: "pipe" | "ignore" | "combined";
---
> stdin?: ReadableStream | "pipe";
> stdout?: "pipe" | "ignore";
> stderr?: "pipe" | "ignore" | "combined";
3952,3954c3950,3952
< get stdin(): WritableStream | undefined;
< get stdout(): ReadableStream | undefined;
< get stderr(): ReadableStream | undefined;
---
> readonly stdin: WritableStream | null;
> readonly stdout: ReadableStream | null;
> readonly stderr: ReadableStream | null;
3959,3961d3956
< readonly __stdinp: WritableStream | null;
< readonly __stdoutp: ReadableStream | null;
< readonly __stderrp: ReadableStream | null;
diff -r types/generated-snapshot/experimental/index.ts bazel-bin/types/definitions/experimental/index.ts
3946,3947d3945
< readonly __stdoutp: ArrayBuffer;
< readonly __stderrp: ArrayBuffer;
3953,3955c3951,3953
< __stdinp?: ReadableStream | "pipe";
< __stdoutp?: "pipe" | "ignore";
< __stderrp?: "pipe" | "ignore" | "combined";
---
> stdin?: ReadableStream | "pipe";
> stdout?: "pipe" | "ignore";
> stderr?: "pipe" | "ignore" | "combined";
3958,3960c3956,3958
< get stdin(): WritableStream | undefined;
< get stdout(): ReadableStream | undefined;
< get stderr(): ReadableStream | undefined;
---
> readonly stdin: WritableStream | null;
> readonly stdout: ReadableStream | null;
> readonly stderr: ReadableStream | null;
3965,3967d3962
< readonly __stdinp: WritableStream | null;
< readonly __stdoutp: ReadableStream | null;
< readonly __stderrp: ReadableStream | null; |
a750650 to
fd3ed7a
Compare
Originally this PR: #6330 but slop PRed because of google auth shenanigans
adds steppromise wrapper with .rollback() api for workflow saga rollbacks. step.do() and step.waitforevent() now return steppromise which lets users register a compensation function before the step rpc fires.