refactor: convert billingOperationStore to discriminated union + owned transition - #14421
refactor: convert billingOperationStore to discriminated union + owned transition#14421christian-byrne wants to merge 2 commits into
Conversation
…d transition BillingOperation had a status field but wasn't a true discriminated union, and handleSuccess/handleFailure/handleTimeout each independently duplicated status update, cleanup, telemetry, and toast logic. Converts BillingOperation into a real discriminated union (Pending/Succeeded/ Failed/TimedOut), and consolidates the three handlers into one transitionToTerminal function that owns the whole pending -> terminal transition, with per-outcome telemetry/toast/side-effect pieces factored out but no longer duplicating the state-update/cleanup/resolve spine. Fixes #14420
📄 Knowledge reviewDosu skipped reviewing this PR because your organization has used its |
🎭 Playwright: ✅ 1763 passed, 0 failed · 4 flaky📊 Browser Reports
🎨 Storybook: ✅ Built — View Storybook📦 Bundle: 8.17 MB gzip 🔴 +307 BDetailsSummary
Category Glance App Entry Points — 3.62 kB (baseline 3.62 kB) • ⚪ 0 BMain entry bundles and manifests
Status: 1 added / 1 removed Graph Workspace — 1.29 MB (baseline 1.29 MB) • ⚪ 0 BGraph editor runtime, canvas, workflow orchestration
Status: 1 added / 1 removed / 1 unchanged Views & Navigation — 111 kB (baseline 111 kB) • ⚪ 0 BTop-level views, pages, and routed surfaces
Status: 12 added / 12 removed / 4 unchanged Panels & Settings — 552 kB (baseline 552 kB) • ⚪ 0 BConfiguration panels, inspectors, and settings screens
Status: 11 added / 11 removed / 15 unchanged User & Accounts — 28.7 kB (baseline 28.7 kB) • ⚪ 0 BAuthentication, profile, and account management bundles
Status: 7 added / 7 removed / 3 unchanged Editors & Dialogs — 124 kB (baseline 124 kB) • ⚪ 0 BModals, dialogs, drawers, and in-app editors
Status: 6 added / 6 removed / 1 unchanged UI Components — 70 kB (baseline 70 kB) • ⚪ 0 BReusable component library chunks
Status: 6 added / 6 removed / 9 unchanged Data & Services — 3.43 MB (baseline 3.43 MB) • 🔴 +440 BStores, services, APIs, and repositories
Status: 14 added / 14 removed / 3 unchanged Utilities & Hooks — 385 kB (baseline 385 kB) • ⚪ 0 BHelpers, composables, and utility bundles
Status: 16 added / 16 removed / 20 unchanged Vendor & Third-Party — 15.7 MB (baseline 15.7 MB) • ⚪ 0 BExternal libraries and shared vendor chunks Status: 16 unchanged Other — 12.6 MB (baseline 12.6 MB) • 🟢 -7 BBundles that do not match a named category
Status: 69 added / 69 removed / 209 unchanged ⚡ Performance Report
Show regressions
All metrics
Historical variance (last 15 runs)
Trend (last 15 commits on main)
Raw data{
"timestamp": "2026-07-30T23:37:40.154Z",
"gitSha": "acf5c1a27cbff8cb9335dfbd5ec9419421180572",
"branch": "refactor/billing-operation-discriminated-union",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2117.572999999993,
"styleRecalcs": 11,
"styleRecalcDurationMs": 7.925,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 552.196,
"heapDeltaBytes": -21702440,
"heapUsedBytes": 46110828,
"domNodes": -278,
"jsHeapTotalBytes": 19914752,
"scriptDurationMs": 18.938000000000002,
"eventListeners": -148,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-idle",
"durationMs": 2056.508000000008,
"styleRecalcs": 10,
"styleRecalcDurationMs": 7.495,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 508.92799999999994,
"heapDeltaBytes": -21645348,
"heapUsedBytes": 46268924,
"domNodes": -278,
"jsHeapTotalBytes": 19914752,
"scriptDurationMs": 16.017000000000003,
"eventListeners": -148,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "canvas-mouse-sweep",
"durationMs": 2052.7470000000108,
"styleRecalcs": 77,
"styleRecalcDurationMs": 42.571999999999996,
"layouts": 12,
"layoutDurationMs": 4.871,
"taskDurationMs": 1052.076,
"heapDeltaBytes": -14781572,
"heapUsedBytes": 53076868,
"domNodes": -284,
"jsHeapTotalBytes": 21487616,
"scriptDurationMs": 121.50299999999999,
"eventListeners": -146,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1822.5830000000087,
"styleRecalcs": 71,
"styleRecalcDurationMs": 31.473000000000003,
"layouts": 12,
"layoutDurationMs": 3.359,
"taskDurationMs": 880.623,
"heapDeltaBytes": -15320344,
"heapUsedBytes": 52471032,
"domNodes": -281,
"jsHeapTotalBytes": 20963328,
"scriptDurationMs": 111.57000000000001,
"eventListeners": -148,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1750.9880000000067,
"styleRecalcs": 30,
"styleRecalcDurationMs": 15.665,
"layouts": 6,
"layoutDurationMs": 0.7410000000000001,
"taskDurationMs": 372.008,
"heapDeltaBytes": 7917444,
"heapUsedBytes": 75779664,
"domNodes": 76,
"jsHeapTotalBytes": 20836352,
"scriptDurationMs": 18.279999999999998,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1738.4559999999283,
"styleRecalcs": 32,
"styleRecalcDurationMs": 14.983999999999998,
"layouts": 6,
"layoutDurationMs": 0.581,
"taskDurationMs": 364.966,
"heapDeltaBytes": 7749292,
"heapUsedBytes": 75834280,
"domNodes": 77,
"jsHeapTotalBytes": 20312064,
"scriptDurationMs": 18.044999999999998,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "dom-widget-clipping",
"durationMs": 591.1510000000249,
"styleRecalcs": 12,
"styleRecalcDurationMs": 7.193999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 376.564,
"heapDeltaBytes": -12307872,
"heapUsedBytes": 55513208,
"domNodes": 20,
"jsHeapTotalBytes": 21622784,
"scriptDurationMs": 55.81700000000001,
"eventListeners": 0,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "dom-widget-clipping",
"durationMs": 580.8739999999943,
"styleRecalcs": 12,
"styleRecalcDurationMs": 7.390999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 381.08299999999997,
"heapDeltaBytes": -12217372,
"heapUsedBytes": 55791884,
"domNodes": 20,
"jsHeapTotalBytes": 21622784,
"scriptDurationMs": 57.31099999999999,
"eventListeners": 0,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-idle",
"durationMs": 2034.9070000000324,
"styleRecalcs": 8,
"styleRecalcDurationMs": 6.544000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 668.194,
"heapDeltaBytes": 7839564,
"heapUsedBytes": 66669148,
"domNodes": -283,
"jsHeapTotalBytes": 4780032,
"scriptDurationMs": 92.888,
"eventListeners": -144,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-idle",
"durationMs": 2035.7689999999593,
"styleRecalcs": 8,
"styleRecalcDurationMs": 6.522,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 660.301,
"heapDeltaBytes": 7504896,
"heapUsedBytes": 66445248,
"domNodes": -283,
"jsHeapTotalBytes": 5304320,
"scriptDurationMs": 91.99799999999999,
"eventListeners": -144,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-pan",
"durationMs": 2170.8880000000477,
"styleRecalcs": 68,
"styleRecalcDurationMs": 14.885,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1263.788,
"heapDeltaBytes": 5931816,
"heapUsedBytes": 65824200,
"domNodes": -285,
"jsHeapTotalBytes": 5005312,
"scriptDurationMs": 425.705,
"eventListeners": -142,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-pan",
"durationMs": 2177.8359999999566,
"styleRecalcs": 69,
"styleRecalcDurationMs": 14.131999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1249.1970000000001,
"heapDeltaBytes": 6128760,
"heapUsedBytes": 65928124,
"domNodes": -283,
"jsHeapTotalBytes": 5267456,
"scriptDurationMs": 416.91,
"eventListeners": -144,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-zoom",
"durationMs": 3130.5880000000457,
"styleRecalcs": 65,
"styleRecalcDurationMs": 15.154999999999998,
"layouts": 60,
"layoutDurationMs": 7.8580000000000005,
"taskDurationMs": 1418.061,
"heapDeltaBytes": 24118756,
"heapUsedBytes": 85736720,
"domNodes": 12,
"jsHeapTotalBytes": 8126464,
"scriptDurationMs": 506.25300000000004,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3164.3890000000283,
"styleRecalcs": 66,
"styleRecalcDurationMs": 16.147,
"layouts": 60,
"layoutDurationMs": 7.896,
"taskDurationMs": 1416.415,
"heapDeltaBytes": 24064932,
"heapUsedBytes": 86005888,
"domNodes": 16,
"jsHeapTotalBytes": 8388608,
"scriptDurationMs": 512.05,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2033.3159999999566,
"styleRecalcs": 9,
"styleRecalcDurationMs": 7.075000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 677.7820000000002,
"heapDeltaBytes": 8357236,
"heapUsedBytes": 69212428,
"domNodes": -281,
"jsHeapTotalBytes": 4780032,
"scriptDurationMs": 94.242,
"eventListeners": -144,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2032.9580000000078,
"styleRecalcs": 8,
"styleRecalcDurationMs": 6.078,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 672.4890000000001,
"heapDeltaBytes": 7690512,
"heapUsedBytes": 68358292,
"domNodes": -285,
"jsHeapTotalBytes": 4255744,
"scriptDurationMs": 95.44400000000002,
"eventListeners": -144,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 580.8280000000536,
"styleRecalcs": 47,
"styleRecalcDurationMs": 10.212,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 398.533,
"heapDeltaBytes": -12119384,
"heapUsedBytes": 55725116,
"domNodes": 20,
"jsHeapTotalBytes": 21884928,
"scriptDurationMs": 122.69999999999999,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 589.4200000000183,
"styleRecalcs": 47,
"styleRecalcDurationMs": 10.768999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 391.129,
"heapDeltaBytes": -12111544,
"heapUsedBytes": 55721232,
"domNodes": 20,
"jsHeapTotalBytes": 21884928,
"scriptDurationMs": 121.553,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-idle",
"durationMs": 2029.0640000000053,
"styleRecalcs": 10,
"styleRecalcDurationMs": 7.790000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 500.818,
"heapDeltaBytes": -21438968,
"heapUsedBytes": 46426116,
"domNodes": -279,
"jsHeapTotalBytes": 19914752,
"scriptDurationMs": 13.264999999999999,
"eventListeners": -148,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-idle",
"durationMs": 2003.2200000000557,
"styleRecalcs": 10,
"styleRecalcDurationMs": 7.556000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 487.361,
"heapDeltaBytes": -22228724,
"heapUsedBytes": 45720612,
"domNodes": 20,
"jsHeapTotalBytes": 21225472,
"scriptDurationMs": 13.306000000000001,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1740.3129999999578,
"styleRecalcs": 77,
"styleRecalcDurationMs": 35.69500000000001,
"layouts": 16,
"layoutDurationMs": 4.325,
"taskDurationMs": 803.2080000000001,
"heapDeltaBytes": -21306704,
"heapUsedBytes": 46894672,
"domNodes": -281,
"jsHeapTotalBytes": 20439040,
"scriptDurationMs": 86.95499999999998,
"eventListeners": -148,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1726.5959999999723,
"styleRecalcs": 76,
"styleRecalcDurationMs": 34.313,
"layouts": 16,
"layoutDurationMs": 4.024,
"taskDurationMs": 768.682,
"heapDeltaBytes": -18020908,
"heapUsedBytes": 50072696,
"domNodes": -279,
"jsHeapTotalBytes": 21487616,
"scriptDurationMs": 82.69600000000001,
"eventListeners": -148,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-transition-enter",
"durationMs": 1384.151000000088,
"styleRecalcs": 18,
"styleRecalcDurationMs": 28.263999999999996,
"layouts": 14,
"layoutDurationMs": 10.872,
"taskDurationMs": 905.9999999999999,
"heapDeltaBytes": 29108172,
"heapUsedBytes": 96122980,
"domNodes": 13673,
"jsHeapTotalBytes": 15990784,
"scriptDurationMs": 33.774000000000015,
"eventListeners": 2371,
"totalBlockingTimeMs": 116,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "viewport-pan-sweep",
"durationMs": 8201.298000000008,
"styleRecalcs": 249,
"styleRecalcDurationMs": 39.443000000000005,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4285.195,
"heapDeltaBytes": 9934724,
"heapUsedBytes": 68648836,
"domNodes": -282,
"jsHeapTotalBytes": 7102464,
"scriptDurationMs": 1293.272,
"eventListeners": -128,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "viewport-pan-sweep",
"durationMs": 8287.08800000004,
"styleRecalcs": 248,
"styleRecalcDurationMs": 38.066,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4319.705,
"heapDeltaBytes": 10685988,
"heapUsedBytes": 69877224,
"domNodes": -282,
"jsHeapTotalBytes": 7069696,
"scriptDurationMs": 1308.797,
"eventListeners": -128,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 17521.37300000004,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 17501.851000000002,
"heapDeltaBytes": -42344540,
"heapUsedBytes": 163749948,
"domNodes": -8316,
"jsHeapTotalBytes": -13832192,
"scriptDurationMs": 555.3580000000001,
"eventListeners": -16385,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-idle",
"durationMs": 17672.888000000057,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 17642.704999999998,
"heapDeltaBytes": -39648024,
"heapUsedBytes": 166721092,
"domNodes": -8312,
"jsHeapTotalBytes": -14622720,
"scriptDurationMs": 561.03,
"eventListeners": -16385,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.780000000000047,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-pan",
"durationMs": 21475.594,
"styleRecalcs": 153,
"styleRecalcDurationMs": 20.406000000000006,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 21432.313000000002,
"heapDeltaBytes": -70858848,
"heapUsedBytes": 153948820,
"domNodes": -8312,
"jsHeapTotalBytes": -16453632,
"scriptDurationMs": 883.3570000000001,
"eventListeners": -16383,
"totalBlockingTimeMs": 455,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-pan",
"durationMs": 21952.225,
"styleRecalcs": 157,
"styleRecalcDurationMs": 20.59999999999995,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 21911.716,
"heapDeltaBytes": -61441456,
"heapUsedBytes": 155959772,
"domNodes": -8312,
"jsHeapTotalBytes": -16195584,
"scriptDurationMs": 891.171,
"eventListeners": -16381,
"totalBlockingTimeMs": 455,
"frameDurationMs": 17.779999999999927,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "workflow-execution",
"durationMs": 457.1419999999762,
"styleRecalcs": 14,
"styleRecalcDurationMs": 22.026999999999997,
"layouts": 3,
"layoutDurationMs": 1.254,
"taskDurationMs": 116.874,
"heapDeltaBytes": -15859228,
"heapUsedBytes": 51021692,
"domNodes": 130,
"jsHeapTotalBytes": 7991296,
"scriptDurationMs": 9.962,
"eventListeners": 67,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "workflow-execution",
"durationMs": 495.0129999999717,
"styleRecalcs": 13,
"styleRecalcDurationMs": 24.874,
"layouts": 3,
"layoutDurationMs": 0.7010000000000001,
"taskDurationMs": 132.529,
"heapDeltaBytes": -15965868,
"heapUsedBytes": 50910736,
"domNodes": 117,
"jsHeapTotalBytes": 7204864,
"scriptDurationMs": 12.567000000000002,
"eventListeners": 65,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.799999999999272
}
]
} |
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 15 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe billing operation store replaces loose status handling with discriminated pending and terminal operation types. Polling success, failure, and timeout paths now use one terminal transition function that updates state, applies side effects, records telemetry, and resolves typed terminal promises. ChangesBilling operation lifecycle
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related issues
Possibly related PRs
Suggested reviewers: Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (1 inconclusive)
✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/platform/workspace/stores/billingOperationStore.ts`:
- Around line 296-326: Update transitionToTerminal so resolveTerminal(opId,
operation) is guaranteed for both succeeded and failure/timeout outcomes, even
when side effects, telemetry, cleanup, or toast handling throws. Use try/finally
around the outcome-specific work, preserving the existing status update and
terminal operation data while ensuring terminal resolver cleanup and promise
settlement always occur; allow the original error to propagate after resolution.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: f3ee93ab-30d6-47ba-9a73-37d1607e0294
📒 Files selected for processing (1)
src/platform/workspace/stores/billingOperationStore.ts
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## main #14421 +/- ##
==========================================
+ Coverage 79.53% 79.57% +0.03%
==========================================
Files 1761 1761
Lines 116914 116869 -45
Branches 38442 39141 +699
==========================================
+ Hits 92993 92996 +3
+ Misses 23362 23308 -54
- Partials 559 565 +6
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 4 files with indirect coverage changes 🚀 New features to boost your workflow:
|
resolveTerminal only ran after applySuccessSideEffects completed without throwing, so a thrown error there (e.g. from a toast/dialog call) leaked the terminal resolver/promise map entries and left any caller awaiting startOperation() hanging forever. poll()'s catch-all silently swallowed the exception, masking the hang. Found by CodeRabbit on #14421.
christian-byrne
left a comment
There was a problem hiding this comment.
Failing unit test and merge conflict
Closing this PR — implementation superseded, bug fix extractedTL;DR: Everything this PR set out to do has been absorbed into What happenedThis PR opened July 30 with two commits:
Between July 31 and August 9, six billing PRs landed on
What was still missingThe Resolution#14964 backfills exactly this: the On PR #14410 (xstate)The Slack discussion that motivated this PR concluded xstate wasn't worth the onboarding cost — that reasoning still holds given how far the plain-TS discriminated union approach has evolved on Thanks for the original refactor — the discriminated union and the |
…hrow A thrown error in the success-path side-effects block (dialog close, settings show, or toast add) silently skips resolveTerminal, leaving any caller awaiting startOperation() hanging forever. poll()'s catch-all swallowed the exception, masking the hang. Adds a try/finally so resolveTerminal runs unconditionally after the success side-effects block. Adds a regression test that throws from the second mockToastAdd call and asserts the terminal promise still resolves to succeeded. Found in the review of #14421. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
Fixes #14420. From the Slack thread on #14396 (xstate/statechart question for billing ops):
BillingOperationhad astatusfield but wasn't a true discriminated union, andhandleSuccess/handleFailure/handleTimeouteach independently duplicated the status-update/cleanup/telemetry/toast spine with small variations.Changes
BillingOperationis now a real discriminated union (PendingOperation | SucceededOperation | FailedOperation | TimedOutOperation), each variant'serrorMessage/actionUrltyped to match what's actually true for that status instead of being nullable-everywhere.transitionToTerminal(opId, outcome)that owns the entire pending -> terminal transition (status update, cleanup, telemetry, side effects, toast, terminal-promise resolution). Per-outcome pieces (telemetry event shape, success side effects, failure toast content) are factored into small functions, but the transition sequencing itself is no longer duplicated three times.Testing
billingOperationStore.test.tstests pass unmodified — the refactor is purely internal representation, no test needed updating.useSubscriptionCheckout,useWorkspaceBilling,useDowngradeToPersonal) pass unmodified.src/platform/workspacesuite: 989 tests passing.pnpm typecheck,pnpm lint,pnpm exec oxfmt --check,pnpm knipall clean.Review focus
The three telemetry-tracking functions (
trackSuccessTelemetry/trackFailureTelemetry) intentionally don't fully collapse into one —BillingTelemetryEvent's type constraints (e.g.downgrade_to_personalevents have notimeoutstage, onlysucceeded/failed) mean some duplication between thefailedandtimeoutbranches is the honest cost of exhaustive discriminated-union typing rather than something to DRY further.