fix(assets): give history cursor recovery epoch ownership#13780
Conversation
Cursor recovery cleared the history walk state without bumping historyFetchEpoch, so a concurrent reader that captured the same epoch (notably an in-flight head refresh fetching offset:0) could pass its stale-guard and act on the just-cleared list. Recovery now bumps the epoch and threads the new value through a mutable walk holder so its caller and loadMoreHistory re-sync to the self-recovery epoch instead of misreading their own reset as external supersession. The reset still runs before the fallback fetch, so a failed offset-0 retry leaves the walk resumable and surfaces the error.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
🎨 Storybook: ✅ Built — View Storybook🎭 Playwright: ✅ 1687 passed, 0 failed · 3 flaky📊 Browser Reports
📦 Bundle Size
⚡ Performance Report
Absolute values
Raw data{
"timestamp": "2026-07-18T01:37:01.607Z",
"gitSha": "0dcc1da7df9c279b8355daf8cb4b12dfbd061dca",
"branch": "matt/be-3420-history-fetch-epoch-ownership",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2014.675000000011,
"styleRecalcs": 6,
"styleRecalcDurationMs": 4.675000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 351.651,
"heapDeltaBytes": 13220832,
"heapUsedBytes": 65108576,
"domNodes": -277,
"jsHeapTotalBytes": 15593472,
"scriptDurationMs": 14.071,
"eventListeners": -199,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-idle",
"durationMs": 2047.2479999999678,
"styleRecalcs": 10,
"styleRecalcDurationMs": 7.023999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 355.547,
"heapDeltaBytes": 12047788,
"heapUsedBytes": 64254872,
"domNodes": -269,
"jsHeapTotalBytes": 16904192,
"scriptDurationMs": 13.113,
"eventListeners": -195,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1809.5619999999712,
"styleRecalcs": 73,
"styleRecalcDurationMs": 38.458,
"layouts": 12,
"layoutDurationMs": 2.951,
"taskDurationMs": 786.841,
"heapDeltaBytes": -13022024,
"heapUsedBytes": 56389064,
"domNodes": -242,
"jsHeapTotalBytes": 19034112,
"scriptDurationMs": 97.085,
"eventListeners": -199,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1716.0020000000031,
"styleRecalcs": 72,
"styleRecalcDurationMs": 27.372999999999994,
"layouts": 12,
"layoutDurationMs": 2.796,
"taskDurationMs": 611.6199999999999,
"heapDeltaBytes": -12590828,
"heapUsedBytes": 56798556,
"domNodes": -242,
"jsHeapTotalBytes": 19034112,
"scriptDurationMs": 80.751,
"eventListeners": -197,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1728.1570000000102,
"styleRecalcs": 31,
"styleRecalcDurationMs": 12.217,
"layouts": 6,
"layoutDurationMs": 0.616,
"taskDurationMs": 291.565,
"heapDeltaBytes": 2890272,
"heapUsedBytes": 55160548,
"domNodes": -239,
"jsHeapTotalBytes": 5632000,
"scriptDurationMs": 14.806000000000001,
"eventListeners": -184,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1754.0520000000015,
"styleRecalcs": 31,
"styleRecalcDurationMs": 14.044999999999998,
"layouts": 6,
"layoutDurationMs": 0.5499999999999999,
"taskDurationMs": 308.22700000000003,
"heapDeltaBytes": -3619108,
"heapUsedBytes": 65774376,
"domNodes": -223,
"jsHeapTotalBytes": 12480512,
"scriptDurationMs": 15.283000000000001,
"eventListeners": -182,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "dom-widget-clipping",
"durationMs": 460.4529999999727,
"styleRecalcs": 13,
"styleRecalcDurationMs": 6.550000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 285.379,
"heapDeltaBytes": -12867668,
"heapUsedBytes": 53105364,
"domNodes": 22,
"jsHeapTotalBytes": 20480000,
"scriptDurationMs": 41.871,
"eventListeners": 0,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "dom-widget-clipping",
"durationMs": 468.8780000000179,
"styleRecalcs": 12,
"styleRecalcDurationMs": 6.037000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 312.054,
"heapDeltaBytes": -24062392,
"heapUsedBytes": 45462240,
"domNodes": -274,
"jsHeapTotalBytes": 6189056,
"scriptDurationMs": 39.549,
"eventListeners": -199,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.663333333333338,
"p95FrameDurationMs": 16.799999999999727
},
{
"name": "large-graph-idle",
"durationMs": 1997.032000000047,
"styleRecalcs": 10,
"styleRecalcDurationMs": 6.879,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 435.66800000000006,
"heapDeltaBytes": -10459996,
"heapUsedBytes": 61271272,
"domNodes": 20,
"jsHeapTotalBytes": 10366976,
"scriptDurationMs": 62.102999999999994,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-idle",
"durationMs": 2020.6469999999968,
"styleRecalcs": 11,
"styleRecalcDurationMs": 6.7139999999999995,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 483.59700000000004,
"heapDeltaBytes": -24800392,
"heapUsedBytes": 59241212,
"domNodes": -260,
"jsHeapTotalBytes": 2445312,
"scriptDurationMs": 66.61,
"eventListeners": -199,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-pan",
"durationMs": 2076.104999999984,
"styleRecalcs": 69,
"styleRecalcDurationMs": 14.973,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 853.907,
"heapDeltaBytes": 9772436,
"heapUsedBytes": 82623012,
"domNodes": 18,
"jsHeapTotalBytes": 12640256,
"scriptDurationMs": 288.311,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2045.7179999999653,
"styleRecalcs": 68,
"styleRecalcDurationMs": 12.988999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 807.927,
"heapDeltaBytes": 8456288,
"heapUsedBytes": 83145796,
"domNodes": 18,
"jsHeapTotalBytes": 10280960,
"scriptDurationMs": 270.117,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-zoom",
"durationMs": 3037.9409999999893,
"styleRecalcs": 63,
"styleRecalcDurationMs": 15.466999999999999,
"layouts": 60,
"layoutDurationMs": 7.010000000000001,
"taskDurationMs": 1033.266,
"heapDeltaBytes": 11729648,
"heapUsedBytes": 66586872,
"domNodes": 8,
"jsHeapTotalBytes": 1835008,
"scriptDurationMs": 364.874,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-zoom",
"durationMs": 3045.363000000009,
"styleRecalcs": 65,
"styleRecalcDurationMs": 15.034,
"layouts": 60,
"layoutDurationMs": 6.111,
"taskDurationMs": 995.1460000000001,
"heapDeltaBytes": 11295180,
"heapUsedBytes": 66375156,
"domNodes": 14,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 346.175,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2019.7489999999902,
"styleRecalcs": 9,
"styleRecalcDurationMs": 5.619000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 445.893,
"heapDeltaBytes": -25982012,
"heapUsedBytes": 60276352,
"domNodes": -263,
"jsHeapTotalBytes": 1396736,
"scriptDurationMs": 63.351000000000006,
"eventListeners": -199,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2008.2109999999602,
"styleRecalcs": 9,
"styleRecalcDurationMs": 5.623999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 422.26900000000006,
"heapDeltaBytes": 7572628,
"heapUsedBytes": 60686904,
"domNodes": -264,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 62.556,
"eventListeners": -199,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 459.8530000000096,
"styleRecalcs": 48,
"styleRecalcDurationMs": 7.969,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 271.284,
"heapDeltaBytes": 8078372,
"heapUsedBytes": 66931572,
"domNodes": 22,
"jsHeapTotalBytes": 17825792,
"scriptDurationMs": 84.099,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 442.6369999999906,
"styleRecalcs": 49,
"styleRecalcDurationMs": 8.513,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 279.298,
"heapDeltaBytes": 6594952,
"heapUsedBytes": 61282028,
"domNodes": 24,
"jsHeapTotalBytes": 13107200,
"scriptDurationMs": 86.13299999999998,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "subgraph-idle",
"durationMs": 2037.6739999999813,
"styleRecalcs": 11,
"styleRecalcDurationMs": 6.593,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 360.653,
"heapDeltaBytes": -1234308,
"heapUsedBytes": 51008812,
"domNodes": -287,
"jsHeapTotalBytes": 16904192,
"scriptDurationMs": 12.285,
"eventListeners": -197,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-idle",
"durationMs": 2014.806999999962,
"styleRecalcs": 11,
"styleRecalcDurationMs": 5.819999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 357.84499999999997,
"heapDeltaBytes": -61264,
"heapUsedBytes": 52196848,
"domNodes": -288,
"jsHeapTotalBytes": 18214912,
"scriptDurationMs": 11.897000000000002,
"eventListeners": -197,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1754.0250000000128,
"styleRecalcs": 78,
"styleRecalcDurationMs": 31.124000000000002,
"layouts": 16,
"layoutDurationMs": 3.585,
"taskDurationMs": 588.479,
"heapDeltaBytes": -16504700,
"heapUsedBytes": 52897860,
"domNodes": -230,
"jsHeapTotalBytes": 20344832,
"scriptDurationMs": 63.775999999999996,
"eventListeners": -195,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1674.133999999981,
"styleRecalcs": 77,
"styleRecalcDurationMs": 25.686,
"layouts": 16,
"layoutDurationMs": 2.8459999999999996,
"taskDurationMs": 504.70900000000006,
"heapDeltaBytes": 11267976,
"heapUsedBytes": 61508056,
"domNodes": 65,
"jsHeapTotalBytes": 19136512,
"scriptDurationMs": 61.794000000000004,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-transition-enter",
"durationMs": 832.0999999999685,
"styleRecalcs": 18,
"styleRecalcDurationMs": 21.188,
"layouts": 5,
"layoutDurationMs": 8.747000000000002,
"taskDurationMs": 582.56,
"heapDeltaBytes": 29907224,
"heapUsedBytes": 101858612,
"domNodes": 13833,
"jsHeapTotalBytes": 16515072,
"scriptDurationMs": 19.354999999999997,
"eventListeners": 2531,
"totalBlockingTimeMs": 126,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "viewport-pan-sweep",
"durationMs": 8070.194999999956,
"styleRecalcs": 251,
"styleRecalcDurationMs": 47.29699999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 2943.3759999999997,
"heapDeltaBytes": 15148544,
"heapUsedBytes": 68200104,
"domNodes": -260,
"jsHeapTotalBytes": 5447680,
"scriptDurationMs": 912.004,
"eventListeners": -183,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333338,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "viewport-pan-sweep",
"durationMs": 8093.021000000022,
"styleRecalcs": 250,
"styleRecalcDurationMs": 44.752,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3043.165,
"heapDeltaBytes": 1251208,
"heapUsedBytes": 73345824,
"domNodes": 20,
"jsHeapTotalBytes": 18931712,
"scriptDurationMs": 968.117,
"eventListeners": 20,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333338,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 8494.784000000038,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 8469.601,
"heapDeltaBytes": -30463164,
"heapUsedBytes": 169925436,
"domNodes": -3300,
"jsHeapTotalBytes": 17825792,
"scriptDurationMs": 433.631,
"eventListeners": -16369,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.80000000000109
},
{
"name": "vue-large-graph-idle",
"durationMs": 8390.650999999934,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 8369.305,
"heapDeltaBytes": -28626596,
"heapUsedBytes": 170751540,
"domNodes": -3300,
"jsHeapTotalBytes": 9961472,
"scriptDurationMs": 428.73,
"eventListeners": -16368,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.216666666666665,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-pan",
"durationMs": 9915.200000000026,
"styleRecalcs": 64,
"styleRecalcDurationMs": 18.122,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 9873.958999999999,
"heapDeltaBytes": -30253312,
"heapUsedBytes": 159293848,
"domNodes": -3291,
"jsHeapTotalBytes": 20447232,
"scriptDurationMs": 667.888,
"eventListeners": -16364,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 10312.904000000002,
"styleRecalcs": 64,
"styleRecalcDurationMs": 18.05799999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 10294.529,
"heapDeltaBytes": -23327252,
"heapUsedBytes": 171864952,
"domNodes": -3300,
"jsHeapTotalBytes": 14680064,
"scriptDurationMs": 699.813,
"eventListeners": -16366,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333338,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "workflow-execution",
"durationMs": 468.4869999999819,
"styleRecalcs": 20,
"styleRecalcDurationMs": 23.134,
"layouts": 4,
"layoutDurationMs": 1.0949999999999998,
"taskDurationMs": 187.54199999999997,
"heapDeltaBytes": -21903588,
"heapUsedBytes": 48672644,
"domNodes": -163,
"jsHeapTotalBytes": 5664768,
"scriptDurationMs": 14.738,
"eventListeners": -130,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999727
},
{
"name": "workflow-execution",
"durationMs": 454.0399999999636,
"styleRecalcs": 18,
"styleRecalcDurationMs": 19.334,
"layouts": 4,
"layoutDurationMs": 0.9160000000000001,
"taskDurationMs": 182.72899999999998,
"heapDeltaBytes": -22150704,
"heapUsedBytes": 48452772,
"domNodes": -170,
"jsHeapTotalBytes": 4878336,
"scriptDurationMs": 17.182999999999996,
"eventListeners": -134,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.700000000000728
}
]
} |
There was a problem hiding this comment.
🔍 Cursor Review — Consolidated panel
Triggered by @mattmillerai.
✅ No high-signal findings.
Panel: 6/8 reviewers contributed findings.
Reviewers that did not contribute: kimi-k2.5:adversarial (empty), kimi-k2.5:edge-case (empty)
Codecov Report✅ All modified and coverable lines are covered by tests. @@ Coverage Diff @@
## matt/fe-962-fe-adopt-cursor-pagination-in-the-generated-tab-jobs-sourced #13780 +/- ##
============================================================================================================
- Coverage 76.26% 75.80% -0.47%
============================================================================================================
Files 1687 1637 -50
Lines 116777 110480 -6297
Branches 37481 36522 -959
============================================================================================================
- Hits 89063 83747 -5316
+ Misses 26969 25852 -1117
- Partials 745 881 +136
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 552 files with indirect coverage changes 🚀 New features to boost your workflow:
|
ELI-5
When the history list hits a stale pagination cursor, it throws the list away and starts over from the top. But it forgot to tell everyone else "I just reset — ignore what you were doing." So if a background "check for new items" refresh was mid-flight, it could wake up and scribble on the freshly-cleared list, or trigger a needless full reload. This teaches the recovery path to raise the shared "generation counter" so those concurrent readers correctly bail — while making sure the recovery's own follow-up fetch isn't fooled into discarding its own result.
Summary
Stacked on #12769 (base branch), addressing a review follow-up deferred from it.
The cursor-recovery path (
fetchHistoryPageWithCursorRecovery) clearedallHistoryItems/loadedIds/loadedJobIds/historyOffset/historyNextCursor/historyCursorModewithout incrementinghistoryFetchEpoch. Every other reset site (fetchHistoryAssets(false),replaceHistoryWithHeadPage) bumps the epoch, so a concurrent reader that captured the same epoch — notably an in-flightdoRefreshHistoryHead, which fetchesoffset:0directly and guards on the epoch — could pass its stale-check and act against the just-cleared list.The clean fix couldn't be a one-liner: bumping the epoch inside recovery also trips
loadMoreHistory's own stale-guard (suppressing a legitimate error) andfetchHistoryAssets's discard check (dropping the recovered page). A single counter can't tell self-recovery apart from external supersession without threading the post-recovery epoch back up.Approach
Recovery now takes ownership of the walk: it bumps
historyFetchEpochand records the new value on a small mutableHistoryWalkholder threaded fromloadMoreHistory→fetchHistoryAssets→ the recovery fetcher. The caller andloadMoreHistoryre-sync to that holder, so they honor their own self-recovery instead of misreading it as an external supersession. The bump + re-stamp happen before the fallbackoffset:0fetch (synchronously, no await between), so even if that retry throws, the holder reflects the new epoch and the error still surfaces with the walk resumable from offset 0.Preserved invariants (all covered by the existing concurrency suite):
isLoadingMorereleased, next scroll not dropped).doRefreshHistoryHeadnow correctly bails when recovery reset the list mid-flight.Tests
discards a concurrent head refresh when cursor recovery resets the walk mid-flight— the core regression (fails without the epoch bump: the un-guarded head refresh fires a spurious full reload).releases isLoadingMore when the offset-0 recovery retry also fails, so the next scroll is not dropped.assetsStore.test.tssuite green (100 tests), including the ~15 cursor/epoch concurrency cases.Verification
vitest run src/stores/assetsStore.test.ts→ 100 passedeslint+oxfmt --checkclean on both changed files