fix: guard workspace auth refresh races#11726
Conversation
📝 WalkthroughWalkthroughAdds token expiry tracking, schedules refresh retries based on remaining lifetime, prevents commits from stale in-flight refreshes, and changes retry exhaustion behavior to preserve valid workspace tokens on transient errors. Tests updated to assert preserved state and improve sessionStorage resilience. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant WorkspaceAuthStore
participant SessionStorage
participant Timer
Client->>WorkspaceAuthStore: trigger refreshToken()
WorkspaceAuthStore->>Timer: schedule retry/backoff (scheduleTokenRefreshRetry)
Timer-->>WorkspaceAuthStore: retry delay elapsed
WorkspaceAuthStore->>WorkspaceAuthStore: check refreshRequestId (stale?) and hasValidWorkspaceToken()
alt request is current
WorkspaceAuthStore->>SessionStorage: set CURRENT_WORKSPACE / TOKEN / EXPIRES
WorkspaceAuthStore-->>Client: commit success
else stale or transient TOKEN_EXCHANGE_FAILED
WorkspaceAuthStore-->>Timer: schedule another retry (if token still valid)
WorkspaceAuthStore-->>Client: preserve current state / do not clear
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Poem
🚥 Pre-merge checks | ✅ 6 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (6 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsGit: Failed to clone repository. Please run the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…auth-refresh-race-v2
🎨 Storybook: ✅ Built — View Storybook |
🎭 Playwright: ✅ 1399 passed, 0 failed📊 Browser Reports
|
📦 Bundle: 5.23 MB gzip 🔴 +362 BDetailsSummary
Category Glance App Entry Points — 22.5 kB (baseline 22.5 kB) • ⚪ 0 BMain entry bundles and manifests
Status: 1 added / 1 removed Graph Workspace — 1.24 MB (baseline 1.24 MB) • ⚪ 0 BGraph editor runtime, canvas, workflow orchestration
Status: 1 added / 1 removed Views & Navigation — 77.7 kB (baseline 77.7 kB) • ⚪ 0 BTop-level views, pages, and routed surfaces
Status: 9 added / 9 removed / 2 unchanged Panels & Settings — 484 kB (baseline 484 kB) • ⚪ 0 BConfiguration panels, inspectors, and settings screens
Status: 10 added / 10 removed / 11 unchanged User & Accounts — 17.4 kB (baseline 17.4 kB) • ⚪ 0 BAuthentication, profile, and account management bundles
Status: 5 added / 5 removed / 2 unchanged Editors & Dialogs — 113 kB (baseline 113 kB) • ⚪ 0 BModals, dialogs, drawers, and in-app editors
Status: 4 added / 4 removed UI Components — 61 kB (baseline 61 kB) • ⚪ 0 BReusable component library chunks
Status: 5 added / 5 removed / 8 unchanged Data & Services — 3.04 MB (baseline 3.04 MB) • 🔴 +1.38 kBStores, services, APIs, and repositories
Status: 13 added / 13 removed / 4 unchanged Utilities & Hooks — 363 kB (baseline 363 kB) • ⚪ 0 BHelpers, composables, and utility bundles
Status: 13 added / 13 removed / 18 unchanged Vendor & Third-Party — 9.88 MB (baseline 9.88 MB) • ⚪ 0 BExternal libraries and shared vendor chunks Status: 16 unchanged Other — 8.83 MB (baseline 8.83 MB) • ⚪ 0 BBundles that do not match a named category
Status: 57 added / 57 removed / 78 unchanged ⚡ Performance Report
All metrics
Historical variance (last 15 runs)
Trend (last 15 commits on main)
Raw data{
"timestamp": "2026-04-28T15:30:39.214Z",
"gitSha": "e7612785d9a60148b43dc686a070bc301a44562d",
"branch": "ben/fe-485-workspace-auth-refresh-race-v2",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 1993.754999999993,
"styleRecalcs": 8,
"styleRecalcDurationMs": 6.613,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 396.84200000000004,
"heapDeltaBytes": 20536688,
"heapUsedBytes": 65135232,
"domNodes": 16,
"jsHeapTotalBytes": 22282240,
"scriptDurationMs": 20.741000000000003,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-idle",
"durationMs": 2016.1959999999794,
"styleRecalcs": 11,
"styleRecalcDurationMs": 9.905000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 416.485,
"heapDeltaBytes": -4072232,
"heapUsedBytes": 46434000,
"domNodes": 22,
"jsHeapTotalBytes": 24641536,
"scriptDurationMs": 24.372000000000003,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "canvas-idle",
"durationMs": 2030.0690000000259,
"styleRecalcs": 10,
"styleRecalcDurationMs": 10.634,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 426.50200000000007,
"heapDeltaBytes": -4000232,
"heapUsedBytes": 46084320,
"domNodes": 20,
"jsHeapTotalBytes": 24117248,
"scriptDurationMs": 26.83,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 2005.1670000000001,
"styleRecalcs": 79,
"styleRecalcDurationMs": 43.126,
"layouts": 12,
"layoutDurationMs": 3.058,
"taskDurationMs": 987.9239999999999,
"heapDeltaBytes": 15747152,
"heapUsedBytes": 59863108,
"domNodes": 63,
"jsHeapTotalBytes": 23330816,
"scriptDurationMs": 124.02199999999999,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 2005.7130000000143,
"styleRecalcs": 84,
"styleRecalcDurationMs": 43.706,
"layouts": 12,
"layoutDurationMs": 3.615,
"taskDurationMs": 1004.5880000000001,
"heapDeltaBytes": 16804676,
"heapUsedBytes": 60562792,
"domNodes": 66,
"jsHeapTotalBytes": 23855104,
"scriptDurationMs": 131.658,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 2033.2540000000563,
"styleRecalcs": 84,
"styleRecalcDurationMs": 46.599000000000004,
"layouts": 12,
"layoutDurationMs": 3.679,
"taskDurationMs": 992.273,
"heapDeltaBytes": 15741860,
"heapUsedBytes": 59795972,
"domNodes": 66,
"jsHeapTotalBytes": 23330816,
"scriptDurationMs": 123.77600000000001,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1722.300999999959,
"styleRecalcs": 32,
"styleRecalcDurationMs": 19.523,
"layouts": 6,
"layoutDurationMs": 0.6479999999999999,
"taskDurationMs": 340.702,
"heapDeltaBytes": 120192,
"heapUsedBytes": 50198284,
"domNodes": 80,
"jsHeapTotalBytes": 24641536,
"scriptDurationMs": 28.918000000000006,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1723.9930000000072,
"styleRecalcs": 31,
"styleRecalcDurationMs": 18.531,
"layouts": 6,
"layoutDurationMs": 0.583,
"taskDurationMs": 345.34,
"heapDeltaBytes": 25059248,
"heapUsedBytes": 68404440,
"domNodes": 78,
"jsHeapTotalBytes": 21233664,
"scriptDurationMs": 29.375000000000004,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1756.7050000000108,
"styleRecalcs": 32,
"styleRecalcDurationMs": 18.109,
"layouts": 6,
"layoutDurationMs": 0.5789999999999998,
"taskDurationMs": 340.98499999999996,
"heapDeltaBytes": 25035484,
"heapUsedBytes": 68707168,
"domNodes": 80,
"jsHeapTotalBytes": 21495808,
"scriptDurationMs": 29.685999999999996,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "dom-widget-clipping",
"durationMs": 535.8960000000081,
"styleRecalcs": 11,
"styleRecalcDurationMs": 8.068,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 332.209,
"heapDeltaBytes": 6754688,
"heapUsedBytes": 50941180,
"domNodes": 18,
"jsHeapTotalBytes": 12582912,
"scriptDurationMs": 55.81999999999999,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999727
},
{
"name": "dom-widget-clipping",
"durationMs": 540.6289999999672,
"styleRecalcs": 11,
"styleRecalcDurationMs": 7.531,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 339.7560000000001,
"heapDeltaBytes": 6988660,
"heapUsedBytes": 50677372,
"domNodes": 18,
"jsHeapTotalBytes": 12582912,
"scriptDurationMs": 55.053000000000004,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.663333333333338,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "dom-widget-clipping",
"durationMs": 521.6739999999618,
"styleRecalcs": 13,
"styleRecalcDurationMs": 8.412,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 320.06600000000003,
"heapDeltaBytes": 7078280,
"heapUsedBytes": 50949676,
"domNodes": 22,
"jsHeapTotalBytes": 13107200,
"scriptDurationMs": 53.33200000000001,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999727
},
{
"name": "large-graph-idle",
"durationMs": 2019.2720000000008,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.466999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 611.2950000000001,
"heapDeltaBytes": -530200,
"heapUsedBytes": 52436624,
"domNodes": -258,
"jsHeapTotalBytes": 16158720,
"scriptDurationMs": 110.22300000000001,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-idle",
"durationMs": 2039.9919999999838,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.774000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 605.134,
"heapDeltaBytes": 3019668,
"heapUsedBytes": 57210936,
"domNodes": -260,
"jsHeapTotalBytes": 16158720,
"scriptDurationMs": 107.516,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-idle",
"durationMs": 2024.630000000002,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.258999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 561.7570000000001,
"heapDeltaBytes": 4240700,
"heapUsedBytes": 56862008,
"domNodes": -261,
"jsHeapTotalBytes": 16158720,
"scriptDurationMs": 105.553,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2103.240000000028,
"styleRecalcs": 68,
"styleRecalcDurationMs": 17.722999999999995,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1113.131,
"heapDeltaBytes": -16057728,
"heapUsedBytes": 48393432,
"domNodes": -264,
"jsHeapTotalBytes": 14962688,
"scriptDurationMs": 386.898,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-pan",
"durationMs": 2118.7229999999886,
"styleRecalcs": 68,
"styleRecalcDurationMs": 17.658000000000005,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1125.1630000000002,
"heapDeltaBytes": 9881820,
"heapUsedBytes": 73253360,
"domNodes": -261,
"jsHeapTotalBytes": 15953920,
"scriptDurationMs": 395.27299999999997,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-pan",
"durationMs": 2129.1290000000345,
"styleRecalcs": 68,
"styleRecalcDurationMs": 16.778000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1112.288,
"heapDeltaBytes": 18709572,
"heapUsedBytes": 73818132,
"domNodes": -265,
"jsHeapTotalBytes": 18460672,
"scriptDurationMs": 390.156,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-zoom",
"durationMs": 3182.278999999994,
"styleRecalcs": 66,
"styleRecalcDurationMs": 19.862999999999996,
"layouts": 60,
"layoutDurationMs": 7.72,
"taskDurationMs": 1367.2300000000002,
"heapDeltaBytes": 6022572,
"heapUsedBytes": 62731940,
"domNodes": -266,
"jsHeapTotalBytes": 17731584,
"scriptDurationMs": 492.93600000000004,
"eventListeners": -123,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3195.707999999968,
"styleRecalcs": 64,
"styleRecalcDurationMs": 18.284,
"layouts": 60,
"layoutDurationMs": 7.898000000000001,
"taskDurationMs": 1363.857,
"heapDeltaBytes": -11546724,
"heapUsedBytes": 54436136,
"domNodes": -269,
"jsHeapTotalBytes": 13914112,
"scriptDurationMs": 481.01899999999995,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3161.811999999941,
"styleRecalcs": 66,
"styleRecalcDurationMs": 21.444000000000003,
"layouts": 60,
"layoutDurationMs": 7.804,
"taskDurationMs": 1387.8999999999999,
"heapDeltaBytes": 2144604,
"heapUsedBytes": 57819524,
"domNodes": -265,
"jsHeapTotalBytes": 16945152,
"scriptDurationMs": 494.738,
"eventListeners": -123,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2007.6589999999896,
"styleRecalcs": 9,
"styleRecalcDurationMs": 9.457,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 593.537,
"heapDeltaBytes": 2532364,
"heapUsedBytes": 58770964,
"domNodes": -263,
"jsHeapTotalBytes": 15896576,
"scriptDurationMs": 100.11200000000001,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "minimap-idle",
"durationMs": 2053.9920000000507,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.671999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 593.8860000000001,
"heapDeltaBytes": 17189488,
"heapUsedBytes": 72951656,
"domNodes": -261,
"jsHeapTotalBytes": 17207296,
"scriptDurationMs": 104.661,
"eventListeners": -123,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "minimap-idle",
"durationMs": 2020.799000000011,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.896,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 563.9970000000001,
"heapDeltaBytes": 2075416,
"heapUsedBytes": 70037104,
"domNodes": -262,
"jsHeapTotalBytes": 10592256,
"scriptDurationMs": 96.82499999999999,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 588.6480000000347,
"styleRecalcs": 48,
"styleRecalcDurationMs": 12.184999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 375.319,
"heapDeltaBytes": 6881052,
"heapUsedBytes": 50894240,
"domNodes": 21,
"jsHeapTotalBytes": 12582912,
"scriptDurationMs": 124.637,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 563.5540000000105,
"styleRecalcs": 47,
"styleRecalcDurationMs": 11.988,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 376.843,
"heapDeltaBytes": 8239548,
"heapUsedBytes": 58627836,
"domNodes": 19,
"jsHeapTotalBytes": 14155776,
"scriptDurationMs": 127.863,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.663333333333338,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 520.2900000000454,
"styleRecalcs": 46,
"styleRecalcDurationMs": 10.386000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 346.90600000000006,
"heapDeltaBytes": 7236136,
"heapUsedBytes": 51262064,
"domNodes": 18,
"jsHeapTotalBytes": 13369344,
"scriptDurationMs": 114.83099999999999,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-idle",
"durationMs": 1990.9420000000182,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.2970000000000015,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 363.75600000000003,
"heapDeltaBytes": 20105200,
"heapUsedBytes": 64380256,
"domNodes": 16,
"jsHeapTotalBytes": 23068672,
"scriptDurationMs": 17.903999999999996,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-idle",
"durationMs": 1992.5790000000347,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.484000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 393.99300000000005,
"heapDeltaBytes": 20889284,
"heapUsedBytes": 64998828,
"domNodes": 20,
"jsHeapTotalBytes": 22806528,
"scriptDurationMs": 24.605999999999995,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-idle",
"durationMs": 1989.7220000000289,
"styleRecalcs": 10,
"styleRecalcDurationMs": 9.32,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 407.309,
"heapDeltaBytes": 19953908,
"heapUsedBytes": 64020672,
"domNodes": 20,
"jsHeapTotalBytes": 22806528,
"scriptDurationMs": 23.317999999999998,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1972.3189999999988,
"styleRecalcs": 84,
"styleRecalcDurationMs": 52.989,
"layouts": 16,
"layoutDurationMs": 5.347,
"taskDurationMs": 1026.7279999999998,
"heapDeltaBytes": 11869888,
"heapUsedBytes": 56341064,
"domNodes": 72,
"jsHeapTotalBytes": 22806528,
"scriptDurationMs": 103.65299999999999,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1991.8810000000349,
"styleRecalcs": 84,
"styleRecalcDurationMs": 46.538999999999994,
"layouts": 16,
"layoutDurationMs": 4.757000000000001,
"taskDurationMs": 941.278,
"heapDeltaBytes": 12315724,
"heapUsedBytes": 56240480,
"domNodes": 72,
"jsHeapTotalBytes": 22806528,
"scriptDurationMs": 103.16,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1975.274000000013,
"styleRecalcs": 84,
"styleRecalcDurationMs": 49.666000000000004,
"layouts": 16,
"layoutDurationMs": 4.869,
"taskDurationMs": 961.8610000000001,
"heapDeltaBytes": 12309248,
"heapUsedBytes": 56604012,
"domNodes": 73,
"jsHeapTotalBytes": 23855104,
"scriptDurationMs": 102.21,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8138.081999999997,
"styleRecalcs": 250,
"styleRecalcDurationMs": 56.603,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4008.1629999999996,
"heapDeltaBytes": -1128652,
"heapUsedBytes": 57874520,
"domNodes": -76,
"jsHeapTotalBytes": 21098496,
"scriptDurationMs": 1305.8129999999999,
"eventListeners": -68,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8129.129999999976,
"styleRecalcs": 249,
"styleRecalcDurationMs": 53.19,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3891.354,
"heapDeltaBytes": 16829572,
"heapUsedBytes": 69975856,
"domNodes": -262,
"jsHeapTotalBytes": 20295680,
"scriptDurationMs": 1271.4669999999999,
"eventListeners": -111,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8188.031000000024,
"styleRecalcs": 251,
"styleRecalcDurationMs": 54.337999999999994,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3838.8100000000004,
"heapDeltaBytes": 22724168,
"heapUsedBytes": 75306144,
"domNodes": -259,
"jsHeapTotalBytes": 20033536,
"scriptDurationMs": 1272.316,
"eventListeners": -111,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.669999999999952,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-idle",
"durationMs": 10788.685999999983,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 10775.095000000001,
"heapDeltaBytes": -50304928,
"heapUsedBytes": 166710780,
"domNodes": -9850,
"jsHeapTotalBytes": 21557248,
"scriptDurationMs": 590.8910000000001,
"eventListeners": -23962,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.220000000000073,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-idle",
"durationMs": 11069.491000000027,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 11020.983,
"heapDeltaBytes": -41324768,
"heapUsedBytes": 156120852,
"domNodes": -9850,
"jsHeapTotalBytes": -28774400,
"scriptDurationMs": 639.161,
"eventListeners": -23965,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "vue-large-graph-idle",
"durationMs": 10914.434000000028,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 10900.104000000001,
"heapDeltaBytes": -64070080,
"heapUsedBytes": 160555368,
"domNodes": -9852,
"jsHeapTotalBytes": 15917056,
"scriptDurationMs": 575.4830000000001,
"eventListeners": -23965,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 13064.82500000004,
"styleRecalcs": 67,
"styleRecalcDurationMs": 18.137999999999987,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 13044.618999999997,
"heapDeltaBytes": -63368692,
"heapUsedBytes": 162550984,
"domNodes": -9850,
"jsHeapTotalBytes": -12259328,
"scriptDurationMs": 892.328,
"eventListeners": -23960,
"totalBlockingTimeMs": 60,
"frameDurationMs": 17.223333333333358,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 13103.02999999999,
"styleRecalcs": 65,
"styleRecalcDurationMs": 18.154000000000003,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 13056.247999999998,
"heapDeltaBytes": -51616988,
"heapUsedBytes": 174358324,
"domNodes": -9850,
"jsHeapTotalBytes": -25452544,
"scriptDurationMs": 892.0920000000001,
"eventListeners": -23957,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-pan",
"durationMs": 12890.629999999986,
"styleRecalcs": 66,
"styleRecalcDurationMs": 18.04,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12862.792999999998,
"heapDeltaBytes": -51065328,
"heapUsedBytes": 161885800,
"domNodes": -9850,
"jsHeapTotalBytes": -12521472,
"scriptDurationMs": 868.139,
"eventListeners": -23959,
"totalBlockingTimeMs": 55,
"frameDurationMs": 17.77333333333336,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "workflow-execution",
"durationMs": 138.00699999995913,
"styleRecalcs": 11,
"styleRecalcDurationMs": 19.349999999999998,
"layouts": 5,
"layoutDurationMs": 1.6700000000000002,
"taskDurationMs": 110.47200000000001,
"heapDeltaBytes": 3457116,
"heapUsedBytes": 55014268,
"domNodes": 149,
"jsHeapTotalBytes": 262144,
"scriptDurationMs": 19.766,
"eventListeners": 37,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.663333333333338,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "workflow-execution",
"durationMs": 462.87900000004356,
"styleRecalcs": 16,
"styleRecalcDurationMs": 22.538000000000004,
"layouts": 5,
"layoutDurationMs": 1.451,
"taskDurationMs": 121.04099999999998,
"heapDeltaBytes": 4960856,
"heapUsedBytes": 49979680,
"domNodes": 154,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 23.534000000000002,
"eventListeners": 71,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "workflow-execution",
"durationMs": 462.89600000000064,
"styleRecalcs": 14,
"styleRecalcDurationMs": 24.495,
"layouts": 5,
"layoutDurationMs": 1.5729999999999997,
"taskDurationMs": 126.384,
"heapDeltaBytes": 4965988,
"heapUsedBytes": 50278148,
"domNodes": 152,
"jsHeapTotalBytes": 262144,
"scriptDurationMs": 25.984,
"eventListeners": 71,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
}
]
} |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/platform/workspace/stores/useWorkspaceAuth.test.ts (1)
734-742: AddEXPIRES_ATassertions in the new persistence/race checksThese updated tests validate
CURRENT_WORKSPACEandTOKEN, but notEXPIRES_AT. Since stale commits also write expiry, asserting it here would close the regression surface for stale overwrite/preserve behavior.Suggested assertion additions
expect(sessionStorage.getItem(WORKSPACE_STORAGE_KEYS.TOKEN)).toBe( 'workspace-token-abc' ) + expect( + sessionStorage.getItem(WORKSPACE_STORAGE_KEYS.EXPIRES_AT) + ).toBeTruthy()expect(sessionStorage.getItem(WORKSPACE_STORAGE_KEYS.TOKEN)).toBe( 'new-workspace-token' ) + expect( + sessionStorage.getItem(WORKSPACE_STORAGE_KEYS.EXPIRES_AT) + ).toBeTruthy()As per coding guidelines "Write tests for all changes, especially bug fixes to catch future regressions."
Also applies to: 823-828, 841-846
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platform/workspace/stores/useWorkspaceAuth.test.ts` around lines 734 - 742, Add assertions that the expiry value is persisted and preserved: after the existing checks on currentWorkspace and workspaceToken, assert that sessionStorage.getItem(WORKSPACE_STORAGE_KEYS.EXPIRES_AT) equals the expected expiry value used in the test setup (e.g. the mock expiry value or mockWorkspaceWithRole.expiresAt string), and if the test stores expiry as JSON ensure you compare the same serialized form; apply the same EXPIRES_AT assertion in the other two assertion blocks referenced (the similar checks around the other transient/persistence scenarios).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/platform/workspace/stores/workspaceAuthStore.ts`:
- Around line 348-354: When the transient-failure branch (isTransientError &&
hasValidWorkspaceToken()) decides to preserve the existing token, do not return
while the last thrown WorkspaceAuthError remains set and no future refresh is
scheduled; instead clear the stored error state (reset the local/instance
"error" / "workspaceAuthError" variable or call the store's clearError helper)
and enqueue a future refresh attempt by invoking the store's refresh scheduling
helper (e.g., scheduleRefresh, scheduleWorkspaceRefresh, or re-arm the existing
timer to call switchWorkspace again after a backoff). This preserves the valid
token, removes the lingering error, and ensures a proactive retry without
changing the branch's intent.
---
Nitpick comments:
In `@src/platform/workspace/stores/useWorkspaceAuth.test.ts`:
- Around line 734-742: Add assertions that the expiry value is persisted and
preserved: after the existing checks on currentWorkspace and workspaceToken,
assert that sessionStorage.getItem(WORKSPACE_STORAGE_KEYS.EXPIRES_AT) equals the
expected expiry value used in the test setup (e.g. the mock expiry value or
mockWorkspaceWithRole.expiresAt string), and if the test stores expiry as JSON
ensure you compare the same serialized form; apply the same EXPIRES_AT assertion
in the other two assertion blocks referenced (the similar checks around the
other transient/persistence scenarios).
🪄 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: CHILL
Plan: Pro
Run ID: 2092171e-46dd-4ce0-9280-881cffb78c34
📒 Files selected for processing (2)
src/platform/workspace/stores/useWorkspaceAuth.test.tssrc/platform/workspace/stores/workspaceAuthStore.ts
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## main #11726 +/- ##
===========================================
- Coverage 69.55% 51.55% -18.01%
===========================================
Files 1485 1376 -109
Lines 83671 70377 -13294
Branches 23029 19567 -3462
===========================================
- Hits 58201 36280 -21921
- Misses 24525 33498 +8973
+ Partials 945 599 -346
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 1003 files with indirect coverage changes 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c3bcc28fbc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (capturedRequestId !== refreshRequestId) { | ||
| console.warn( | ||
| 'Aborting stale workspace switch: workspace context changed before commit' | ||
| ) | ||
| return |
There was a problem hiding this comment.
Don't discard old-workspace refresh when new switch fails
This guard drops any in-flight refresh response as soon as refreshRequestId changes, but refreshRequestId is incremented before a new switchWorkspace request is known to succeed. If refreshToken() for workspace A is in flight, then switchWorkspace('B') fails (e.g. 403), workspace A remains active but the successful refresh response for A is discarded here; refreshToken() then returns without committing a new token/schedule, so A can continue with an aging token until expiration. The stale check should also account for whether currentWorkspace actually changed away from the refreshed workspace before aborting.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/platform/workspace/stores/useWorkspaceAuth.test.ts (1)
825-833: Strengthen stale-race regression by assertingEXPIRES_ATis not clobberedThe test currently protects
CURRENT_WORKSPACEandTOKEN, but notEXPIRES_AT. Using distinct expiries for “new” vs “stale” responses would catch expiry-key overwrites too.✅ Suggested test tightening
- mockFetch.mockResolvedValueOnce({ + const newExpiry = new Date(Date.now() + 7200 * 1000).toISOString() + mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ ...mockTokenResponse, token: 'new-workspace-token', + expires_at: newExpiry, workspace: newWorkspace }) }) @@ expect(sessionStorage.getItem(WORKSPACE_STORAGE_KEYS.TOKEN)).toBe( 'new-workspace-token' ) + const expectedNewExpiryMs = new Date(newExpiry).getTime().toString() + expect(sessionStorage.getItem(WORKSPACE_STORAGE_KEYS.EXPIRES_AT)).toBe( + expectedNewExpiryMs + ) @@ expect(sessionStorage.getItem(WORKSPACE_STORAGE_KEYS.TOKEN)).toBe( 'new-workspace-token' ) + expect(sessionStorage.getItem(WORKSPACE_STORAGE_KEYS.EXPIRES_AT)).toBe( + expectedNewExpiryMs + )As per coding guidelines, "Write tests for all changes, especially bug fixes to catch future regressions".
Also applies to: 857-862
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/platform/workspace/stores/useWorkspaceAuth.test.ts` around lines 825 - 833, The test currently only protects CURRENT_WORKSPACE and TOKEN but not EXPIRES_AT; update the mocked responses (the mockTokenResponse used in mockFetch.mockResolvedValueOnce and the subsequent mock) to include distinct expiry values (e.g., expiresAt: 'stale-ts' vs 'new-ts' or numeric EXPIRES_AT) for the "stale" and "new" responses and then add assertions that the stored EXPIRES_AT value (the key your code writes, e.g., EXPIRES_AT) equals the expected new expiry after the race-resolve path and was not clobbered by the stale response; modify both places the mock is set (the block using mockTokenResponse at the shown diff and the later similar mock at the other occurrence) and add an expect(...) asserting EXPIRES_AT remains the new value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/platform/workspace/stores/workspaceAuthStore.ts`:
- Around line 202-203: The loading state can be cleared by stale overlapping
calls to switchWorkspace; modify switchWorkspace to generate a unique request
id/token at start (set isLoading.value = true; error.value = null), store it on
the store (e.g., currentSwitchId), and capture it in the async call; only clear
isLoading.value and update error.value when the captured id matches the
store.currentSwitchId. Apply the same request-id check to the success and error
branches where isLoading is set to false (the code around switchWorkspace and
the lines that currently set isLoading.value = false / error.value = ...).
---
Nitpick comments:
In `@src/platform/workspace/stores/useWorkspaceAuth.test.ts`:
- Around line 825-833: The test currently only protects CURRENT_WORKSPACE and
TOKEN but not EXPIRES_AT; update the mocked responses (the mockTokenResponse
used in mockFetch.mockResolvedValueOnce and the subsequent mock) to include
distinct expiry values (e.g., expiresAt: 'stale-ts' vs 'new-ts' or numeric
EXPIRES_AT) for the "stale" and "new" responses and then add assertions that the
stored EXPIRES_AT value (the key your code writes, e.g., EXPIRES_AT) equals the
expected new expiry after the race-resolve path and was not clobbered by the
stale response; modify both places the mock is set (the block using
mockTokenResponse at the shown diff and the later similar mock at the other
occurrence) and add an expect(...) asserting EXPIRES_AT remains the new value.
🪄 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: CHILL
Plan: Pro
Run ID: ed5c4973-a24b-48e6-b915-1eb4a76db3c3
📒 Files selected for processing (2)
src/platform/workspace/stores/useWorkspaceAuth.test.tssrc/platform/workspace/stores/workspaceAuthStore.ts
| isLoading.value = true | ||
| error.value = null |
There was a problem hiding this comment.
isLoading can be cleared by a stale request while a newer switch is still pending
Line 306 always sets isLoading to false, so with overlapping switchWorkspace calls, an older stale request can hide loading state for the active request.
💡 Proposed fix (track in-flight switches)
// Timer state
let refreshTimerId: ReturnType<typeof setTimeout> | null = null
+ let inFlightSwitchCount = 0
@@
- isLoading.value = true
+ inFlightSwitchCount += 1
+ isLoading.value = true
error.value = null
@@
} finally {
- isLoading.value = false
+ inFlightSwitchCount = Math.max(0, inFlightSwitchCount - 1)
+ isLoading.value = inFlightSwitchCount > 0
}
}Also applies to: 305-307
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/platform/workspace/stores/workspaceAuthStore.ts` around lines 202 - 203,
The loading state can be cleared by stale overlapping calls to switchWorkspace;
modify switchWorkspace to generate a unique request id/token at start (set
isLoading.value = true; error.value = null), store it on the store (e.g.,
currentSwitchId), and capture it in the async call; only clear isLoading.value
and update error.value when the captured id matches the store.currentSwitchId.
Apply the same request-id check to the success and error branches where
isLoading is set to false (the code around switchWorkspace and the lines that
currently set isLoading.value = false / error.value = ...).
Summary
Fixes FE-485.
This updates workspace auth refresh handling so stale in-flight refresh responses cannot overwrite a newer workspace context, and exhausted transient token exchange failures preserve the existing workspace context while its token is still valid.
Changes
switchWorkspacewrites workspace state, workspace token,error, orsessionStorage.sessionStoragepreservation.Browser / E2E coverage
No Playwright test was added because this bug is in the Pinia store race between mocked token-exchange promises, request IDs, token expiry, and
sessionStoragecommits. The deterministic unit spec directly controls the ordering that is not practical to force through the browser without real auth/session infrastructure and artificial network timing hooks.Validation
pnpm format -- src/platform/workspace/stores/workspaceAuthStore.ts src/platform/workspace/stores/useWorkspaceAuth.test.tspnpm exec vitest run src/platform/workspace/stores/useWorkspaceAuth.test.tspnpm exec eslint src/platform/workspace/stores/workspaceAuthStore.ts src/platform/workspace/stores/useWorkspaceAuth.test.tspnpm exec oxlint src/platform/workspace/stores/workspaceAuthStore.ts src/platform/workspace/stores/useWorkspaceAuth.test.ts --type-awarepnpm exec vue-tsc --noEmit --pretty false┆Issue is synchronized with this Notion page by Unito