feat(assets): adopt cursor pagination in the Generated tab jobs walk#12769
feat(assets): adopt cursor pagination in the Generated tab jobs walk#12769mattmillerai wants to merge 20 commits into
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds cursor-aware Jobs pagination (after cursor with offset fallback), throws JobsApiError on non-OK responses, rewrites assetsStore history paging with cursor state and head-refresh semantics (refreshHistoryHead), and updates callers/tests to use fetchHistoryPage and refreshHistoryHead call shapes. ChangesCursor Pagination & History Refresh
Sequence DiagramsequenceDiagram
participant Component as GraphView/Queue
participant Store as assetsStore
participant API as fetchHistoryPage
participant Jobs as /jobs API
Component->>Store: refreshHistoryHead()
Store->>API: { offset: 0 }
API->>Jobs: GET /jobs?offset=0
Jobs-->>API: { items, has_more, next_cursor }
API-->>Store: FetchHistoryPageResult (items, hasMore, nextCursor)
Store->>Store: mergeHistoryAssets or replaceHistoryWithHeadPage
Store-->>Component: updated history
Note over Store: loadMoreHistory uses cursor when available
Component->>Store: loadMoreHistory()
alt nextCursor available
Store->>API: { after: nextCursor }
else fallback to offset
Store->>API: { offset: lastOffset }
end
API->>Jobs: GET /jobs?after=cursor or offset=N
Jobs-->>API: { items, has_more, next_cursor }
API-->>Store: FetchHistoryPageResult
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 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 unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (5)
src/platform/remote/comfyui/jobs/fetchJobs.ts (1)
68-70: 💤 Low valueConsider including status text in error message for easier debugging.
Adding
res.statusTextwould provide more context when investigating failures (e.g., distinguishing "400 Bad Request" from "400 INVALID_CURSOR").♻️ Suggested improvement
if (!res.ok) { - throw new Error(`[Jobs API] Failed to fetch jobs: ${res.status}`) + throw new Error(`[Jobs API] Failed to fetch jobs: ${res.status} ${res.statusText}`) }🤖 Prompt for 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. In `@src/platform/remote/comfyui/jobs/fetchJobs.ts` around lines 68 - 70, Update the error thrown on non-OK responses in the fetchJobs flow so it includes both res.status and res.statusText for better context; locate the check that throws new Error(`[Jobs API] Failed to fetch jobs: ${res.status}`) (in src/platform/remote/comfyui/jobs/fetchJobs.ts / the fetchJobs handler) and change the message to include res.statusText (e.g., combine status and statusText into the thrown Error message) so failures show both numeric code and human-readable status.src/views/GraphView.test.ts (1)
103-107: ⚡ Quick winAdd behavioral assertions for the new history-refresh path.
This mock shape update is necessary, but the suite still doesn’t verify that
status/execution_successflows callassetsStore.refreshHistoryHead()
under the assets-open or linear-mode conditions introduced inGraphView.vue.
Please add focused event-path tests so this integration can’t regress silently.As per coding guidelines,
src/**/*.test.tsshould “write tests for all
changes, especially bug fixes to catch future regressions” and aim for
behavioral coverage of critical/new features.🤖 Prompt for 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. In `@src/views/GraphView.test.ts` around lines 103 - 107, The test suite currently mocks useAssetsStore but lacks assertions that the new history-refresh code path in GraphView.vue calls assetsStore.refreshHistoryHead; add focused behavioral tests in GraphView.test.ts that simulate the two event flows (status change and execution_success) under the two relevant conditions (assets-open and linear-mode) and assert that the mocked refreshHistoryHead() is called (and not called when conditions are false). Locate and extend the existing mocked useAssetsStore (updateHistory and refreshHistoryHead) and add tests that trigger the component events or lifecycle/state changes used by GraphView (e.g., emitting the status/execution_success events or toggling the assets-open/linear-mode flags) and verify refreshHistoryHead was invoked the expected number of times.Source: Coding guidelines
src/stores/assetsStore.test.ts (1)
30-47: 💤 Low valueConsider using a function declaration for the helper.
Per coding guidelines, prefer function declarations over function expressions for pure functions. This helper could be declared as:
-const mockHistoryPage = ( - jobs: JobListItem[], - { - hasMore = false, - nextCursor, - offset = 0, - total = jobs.length, - limit = 200 - }: Partial<Omit<FetchHistoryPageResult, 'jobs'>> = {} -): FetchHistoryPageResult => ({ - jobs, - total, - offset, - limit, - hasMore, - nextCursor -}) +function mockHistoryPage( + jobs: JobListItem[], + { + hasMore = false, + nextCursor, + offset = 0, + total = jobs.length, + limit = 200 + }: Partial<Omit<FetchHistoryPageResult, 'jobs'>> = {} +): FetchHistoryPageResult { + return { + jobs, + total, + offset, + limit, + hasMore, + nextCursor + } +}Based on learnings: "Prefer pure function declarations over function expressions (e.g., use function foo() { ... } instead of const foo = () => { ... }) for pure functions in the repository."
🤖 Prompt for 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. In `@src/stores/assetsStore.test.ts` around lines 30 - 47, The helper is defined as a const arrow function (mockHistoryPage) but the repo prefers pure function declarations; change the arrow function to a named function declaration (function mockHistoryPage(jobs: JobListItem[], opts: Partial<Omit<FetchHistoryPageResult, 'jobs'>> = {}) : FetchHistoryPageResult) preserving the same parameter defaults and return structure (jobs, total, offset, limit, hasMore, nextCursor) and keep types JobListItem and FetchHistoryPageResult intact so callers/tests remain unchanged.Source: Learnings
src/stores/assetsStore.ts (2)
157-178: 💤 Low valueConsider binary search for sorted insertion.
The current implementation uses
findIndex(O(n)) for each insertion, resulting in O(n·m) complexity. Since the list maintains sorted order bycreated_at, a binary search would reduce this to O(m·log n). Additionally, the repeatednew Date()calls insidefindIndexcreate many short-lived objects.This is acceptable given the MAX_HISTORY_ITEMS=1000 bound, but could be optimized if refresh latency becomes noticeable.
🤖 Prompt for 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. In `@src/stores/assetsStore.ts` around lines 157 - 178, mergeHistoryAssets currently uses findIndex with repeated new Date() calls, giving O(n·m) behavior and many short-lived Date objects; replace the linear search with a binary search over allHistoryItems.value (which is sorted by created_at) to compute insertIndex in O(log n), and convert each asset.created_at and existing item.created_at to timestamps once (e.g., const assetTime = +new Date(asset.created_at ?? 0)) to avoid repeated Date construction; keep skipping duplicates via loadedIds and maintain the same push/splice logic (use the computed insertIndex === -1 meaning append) and respect MAX_HISTORY_ITEMS if applicable.
335-335: Fix:job.idmatchesasset.idfor this overlap check
loadedIdsis populated withAssetItem.idfrommapHistoryToAssets, andmapTaskOutputToAssetItemsetsid: taskItem.jobId;TaskItemImpl.jobIdis derived directly fromjob.id, sopage.jobs.some(job => loadedIds.has(job.id))compares the same id space.The only reason
reachesLoadedItemscould befalsedespite overlap is filtering:mapHistoryToAssetsonly addscompletedjobs that havepreview_output, whilepage.jobsincludes other terminal statuses/jobs.🤖 Prompt for 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. In `@src/stores/assetsStore.ts` at line 335, reachesLoadedItems is false due to mapHistoryToAssets only adding AssetItems for completed jobs with preview_output, so loadedIds lacks IDs for other terminal jobs present in page.jobs; update mapHistoryToAssets (and/or mapTaskOutputToAssetItem where AssetItem.id is set) to also include terminal jobs from history even if they lack preview_output (or otherwise ensure loadedIds includes job.id for those terminal statuses), so page.jobs.some(job => loadedIds.has(job.id)) correctly detects overlap with TaskItemImpl.jobId/job.id.
🤖 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.
Nitpick comments:
In `@src/platform/remote/comfyui/jobs/fetchJobs.ts`:
- Around line 68-70: Update the error thrown on non-OK responses in the
fetchJobs flow so it includes both res.status and res.statusText for better
context; locate the check that throws new Error(`[Jobs API] Failed to fetch
jobs: ${res.status}`) (in src/platform/remote/comfyui/jobs/fetchJobs.ts / the
fetchJobs handler) and change the message to include res.statusText (e.g.,
combine status and statusText into the thrown Error message) so failures show
both numeric code and human-readable status.
In `@src/stores/assetsStore.test.ts`:
- Around line 30-47: The helper is defined as a const arrow function
(mockHistoryPage) but the repo prefers pure function declarations; change the
arrow function to a named function declaration (function mockHistoryPage(jobs:
JobListItem[], opts: Partial<Omit<FetchHistoryPageResult, 'jobs'>> = {}) :
FetchHistoryPageResult) preserving the same parameter defaults and return
structure (jobs, total, offset, limit, hasMore, nextCursor) and keep types
JobListItem and FetchHistoryPageResult intact so callers/tests remain unchanged.
In `@src/stores/assetsStore.ts`:
- Around line 157-178: mergeHistoryAssets currently uses findIndex with repeated
new Date() calls, giving O(n·m) behavior and many short-lived Date objects;
replace the linear search with a binary search over allHistoryItems.value (which
is sorted by created_at) to compute insertIndex in O(log n), and convert each
asset.created_at and existing item.created_at to timestamps once (e.g., const
assetTime = +new Date(asset.created_at ?? 0)) to avoid repeated Date
construction; keep skipping duplicates via loadedIds and maintain the same
push/splice logic (use the computed insertIndex === -1 meaning append) and
respect MAX_HISTORY_ITEMS if applicable.
- Line 335: reachesLoadedItems is false due to mapHistoryToAssets only adding
AssetItems for completed jobs with preview_output, so loadedIds lacks IDs for
other terminal jobs present in page.jobs; update mapHistoryToAssets (and/or
mapTaskOutputToAssetItem where AssetItem.id is set) to also include terminal
jobs from history even if they lack preview_output (or otherwise ensure
loadedIds includes job.id for those terminal statuses), so page.jobs.some(job =>
loadedIds.has(job.id)) correctly detects overlap with TaskItemImpl.jobId/job.id.
In `@src/views/GraphView.test.ts`:
- Around line 103-107: The test suite currently mocks useAssetsStore but lacks
assertions that the new history-refresh code path in GraphView.vue calls
assetsStore.refreshHistoryHead; add focused behavioral tests in
GraphView.test.ts that simulate the two event flows (status change and
execution_success) under the two relevant conditions (assets-open and
linear-mode) and assert that the mocked refreshHistoryHead() is called (and not
called when conditions are false). Locate and extend the existing mocked
useAssetsStore (updateHistory and refreshHistoryHead) and add tests that trigger
the component events or lifecycle/state changes used by GraphView (e.g.,
emitting the status/execution_success events or toggling the
assets-open/linear-mode flags) and verify refreshHistoryHead was invoked the
expected number of times.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 0a3e3d99-e00b-4a26-b511-1783242969cf
📒 Files selected for processing (11)
src/components/queue/QueueProgressOverlay.vuesrc/platform/missingMedia/missingMediaAssetResolver.test.tssrc/platform/missingMedia/missingMediaAssetResolver.tssrc/platform/missingMedia/missingMediaScan.test.tssrc/platform/remote/comfyui/jobs/fetchJobs.test.tssrc/platform/remote/comfyui/jobs/fetchJobs.tssrc/platform/remote/comfyui/jobs/jobTypes.tssrc/stores/assetsStore.test.tssrc/stores/assetsStore.tssrc/views/GraphView.test.tssrc/views/GraphView.vue
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## main #12769 +/- ##
==========================================
+ Coverage 76.08% 76.10% +0.02%
==========================================
Files 1574 1574
Lines 101628 101701 +73
Branches 31718 31119 -599
==========================================
+ Hits 77319 77399 +80
+ Misses 23483 23476 -7
Partials 826 826
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 5 files with indirect coverage changes 🚀 New features to boost your workflow:
|
Applies verified findings from multi-model review of #12769: - next_cursor accepts null (z.string().nullish()) so a backend serializing absent-as-null can't crash the whole history fetch at the zod boundary - JobsApiError carries the HTTP status and response body; the store's cursor recovery now drops the cursor for the offset fallback only on a 400 (stale/rejected cursor) and only while its walk is still current — transient failures and superseded continuations propagate so a valid cursor is never discarded - an empty page without a cursor is terminal (offset paging cannot advance by zero), while an empty page that still mints a cursor keeps walking - refreshHistoryHead coalesces event bursts into the in-flight refresh plus exactly one trailing refresh, so a job completing mid-refresh is picked up instead of being satisfied by a response dispatched before it existed
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/stores/assetsStore.ts (2)
372-377:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDetect overlap from walked history rows, not only mapped asset IDs.
Line 372 compares raw
job.idvalues againstloadedIds, butloadedIdsis filled only aftermapHistoryToAssets()drops non-preview history rows. If the refreshed head page reaches the already-walked portion through a filtered-out job,reachesLoadedItemsstays false and Lines 373-377 unnecessarily fall back toupdateHistory(), which resets the user's scroll position even though the page already overlaps the loaded window. Track a separate set of walked history job IDs for this check instead.🤖 Prompt for 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. In `@src/stores/assetsStore.ts` around lines 372 - 377, The current overlap check uses loadedIds (populated after mapHistoryToAssets filters out non-preview rows) so reachesLoadedItems can be false when the refreshed page overlaps already-walked rows that were filtered; introduce and maintain a separate set (e.g., walkedHistoryIds) of raw history job IDs as you walk history, use that set instead of loadedIds when computing reachesLoadedItems in the block containing the existing reachesLoadedItems / updateHistory logic, and ensure walkedHistoryIds is kept in sync whenever history is advanced or reset so the overlap detection correctly avoids unnecessary calls to updateHistory().
373-378:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftAvoid clearing the current walk before the restart succeeds.
Line 377 calls
updateHistory(), and that path clearshistoryOffset,historyNextCursor,allHistoryItems, andloadedIdsbefore the fetch runs. If this restart hits a transient error, the oldhistoryAssetslist stays visible but the internal pagination state is already gone, so the nextloadMoreHistory()starts again at offset 0 and can collapse the list back to the first page. Keep the existing walk state until the replacement fetch succeeds, or restore it on failure.🤖 Prompt for 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. In `@src/stores/assetsStore.ts` around lines 373 - 378, The current restart flow in updateHistory() clears historyOffset, historyNextCursor, allHistoryItems, and loadedIds immediately, which can leave UI state inconsistent if the replacement fetch fails; change updateHistory()/the restart path so it first performs the fetch into local/temp variables (e.g., tempOffset/tempCursor/tempAllHistoryItems/tempLoadedIds) without mutating the store, and only replace the store fields (historyOffset, historyNextCursor, allHistoryItems, loadedIds) after the fetch succeeds; on fetch failure, leave the existing store state intact (or explicitly roll back to the previous values) so loadMoreHistory() won't restart from an empty pagination state unexpectedly.
🤖 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.
Outside diff comments:
In `@src/stores/assetsStore.ts`:
- Around line 372-377: The current overlap check uses loadedIds (populated after
mapHistoryToAssets filters out non-preview rows) so reachesLoadedItems can be
false when the refreshed page overlaps already-walked rows that were filtered;
introduce and maintain a separate set (e.g., walkedHistoryIds) of raw history
job IDs as you walk history, use that set instead of loadedIds when computing
reachesLoadedItems in the block containing the existing reachesLoadedItems /
updateHistory logic, and ensure walkedHistoryIds is kept in sync whenever
history is advanced or reset so the overlap detection correctly avoids
unnecessary calls to updateHistory().
- Around line 373-378: The current restart flow in updateHistory() clears
historyOffset, historyNextCursor, allHistoryItems, and loadedIds immediately,
which can leave UI state inconsistent if the replacement fetch fails; change
updateHistory()/the restart path so it first performs the fetch into local/temp
variables (e.g., tempOffset/tempCursor/tempAllHistoryItems/tempLoadedIds)
without mutating the store, and only replace the store fields (historyOffset,
historyNextCursor, allHistoryItems, loadedIds) after the fetch succeeds; on
fetch failure, leave the existing store state intact (or explicitly roll back to
the previous values) so loadMoreHistory() won't restart from an empty pagination
state unexpectedly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: f2a718eb-f3b2-40c5-9574-1b85d825c6f6
📒 Files selected for processing (5)
src/platform/remote/comfyui/jobs/fetchJobs.test.tssrc/platform/remote/comfyui/jobs/fetchJobs.tssrc/platform/remote/comfyui/jobs/jobTypes.tssrc/stores/assetsStore.test.tssrc/stores/assetsStore.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- src/platform/remote/comfyui/jobs/jobTypes.ts
- src/platform/remote/comfyui/jobs/fetchJobs.test.ts
- src/platform/remote/comfyui/jobs/fetchJobs.ts
- src/stores/assetsStore.test.ts
Calls the reusable Comfy-Org/github-workflows cursor-review.yml (single source of truth for panel, judge, prompts, scripts) instead of a standalone copy. Label-gated to the team; secret-bearing jobs skip fork PRs. Judge overridden to Opus 4.8.
dante01yoon
left a comment
There was a problem hiding this comment.
Read-only review at 5e95fcc (not approving, per request). This is the careful one, and overall it's well-reasoned: the epoch counter invalidating superseded walks, the coalescing single-trailing refreshHistoryHead, the gap-restart-vs-whole-timeline-replace decisions, and the contained throw-propagation are all solid and well-tested. I verified the "Breaking" change is actually contained — api.getHistory/api.getQueue both still try/catch and return empty, and the store's updateHistory/loadMoreHistory wrappers catch as well.
Two findings inline: one issue: (a stuck-cursor guard the sibling #12774 has but this walk lacks) and one question: on the OSS missing-media behavior change.
Heads-up: your open #12464 (FE-740) and #12398 (FE-746) also touch assetsStore.ts / missingMediaAssetResolver.ts — no duplicate logic, but plan for a rebase whichever lands second.
There was a problem hiding this comment.
Re-review at 7e95419
- issue (stuck-cursor guard) — fixed in 2264e4f. The history jobs walk now mirrors the flat-output walk's non-advancing-cursor guard, and I confirmed it actually terminates in production (
loadMoreHistoryearly-returns on!hasMoreHistory) with a genuine red-green regression test. Thread replied. - question (OSS missing-media throw) — confirmed intentional; the throw is load-bearing for cursor recovery (400
INVALID_CURSOR→ drop cursor vs transient → retry), and the cloud-branch symmetry is the point. Accepted. Thread replied.
No new issues in the delta since 5e95fcc (the rest is a rebase onto main). CI green on this head (39 success / 9 skipped). Threads left open for you to resolve.
022a125
…e-generated-tab-jobs-sourced
|
Assigned @DrJKL or @christian-byrne as second approver prior to merging as discussed. |
DrJKL
left a comment
There was a problem hiding this comment.
Review of the cursor-pagination changes. Findings are inline: one correctness issue in the rejected-cursor fallback, one simplification on the head-refresh coalescing, plus minor type-safety, schema, comment, and test items (with suggestions where applicable).
Co-authored-by: Alexander Brown <drjkl@comfy.org>
89fdbcd
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
…r superseded loadMore
When a pagination cursor is rejected, fall back to offset 0 and replace the loaded list rather than using the stale client-side offset. The previous offset drifted when items were deleted server-side, causing the recovery page to skip or duplicate rows. Acceptable tradeoff: this rare fallback resets scroll position.
# Conflicts: # src/stores/assetsStore.test.ts
| return | ||
| } | ||
|
|
||
| if (page.hasMore) { |
There was a problem hiding this comment.
issue: in offset-fallback mode (cursorless backends, the Playwright fixture), this merge branch prepends new head jobs but never advances historyOffset. The server timeline shifts down by the number of new completions, so the next offset-mode loadMoreHistory() re-requests an overlapping window, reintroducing the offset drift this PR removes (rows past the shifted window get silently skipped). Before this PR, completion did a full updateHistory() reset and did not drift. Could the offset branch advance historyOffset by the merged-new count, or force a full reset when historyNextCursor == null?
| loadedIds.clear() | ||
| newAssets.forEach((asset) => loadedIds.add(asset.id)) | ||
| historyOffset.value = page.jobs.length | ||
| historyNextCursor.value = page.nextCursor ?? null |
There was a problem hiding this comment.
suggestion (non-blocking): every status / execution_success event re-fetches the full head page (BATCH_SIZE 200) and runs mapHistoryToAssets over all 200 jobs (a TaskItemImpl + parseTaskOutput each, then a full sort) to surface ~1 new row. During a batch run this fires per finished job. Since the merge already dedupes by loadedIds, filtering page.jobs to non-seen ids before mapping/parsing would cut steady-state cost from O(p log p) to O(new rows).
| * @param loadMore - true for pagination (append), false for initial load (replace) | ||
| * Insert assets in sorted order (newest first), skipping already-loaded ids | ||
| */ | ||
| const mergeHistoryAssets = (newAssets: AssetItem[]) => { |
There was a problem hiding this comment.
suggestion (non-blocking): per-item findIndex + splice here is O(page x n), and the predicate re-parses new Date(item.created_at) on the same items repeatedly. Both the incoming page and allHistoryItems are sorted newest-first, so a two-pointer merge (O(page + n)) plus caching getTime() once would remove the repeated parsing on the scroll hot path.
| } | ||
| loadedIds.add(asset.id) | ||
|
|
||
| const assetTime = new Date(asset.created_at ?? 0).getTime() |
There was a problem hiding this comment.
question: assetTime = new Date(asset.created_at ?? 0).getTime(). If a completed job has a null created_at, assetTime is 0, findIndex returns -1, and the newest job is pushed to the bottom (and may be trimmed away) instead of prepended. Can a terminal job ever lack created_at? If so, treating missing as newest would fix it.
| return | ||
| } | ||
|
|
||
| historyError.value = null |
There was a problem hiding this comment.
suggestion (non-blocking): refreshHistoryHead is fire-and-forget from completion events and shares historyError with loadMoreHistory with no lock. An interleaved head refresh that clears historyError on entry can hide a real load-more error (or vice versa). A separate error ref for the background head refresh would avoid the cross-talk.
| 'prompt_1', | ||
| 'prompt_2' | ||
| ]) | ||
|
|
There was a problem hiding this comment.
suggestion (non-blocking): the transient-error tests assert only historyError instanceof Error. Since the code branches on JobsApiError.status === 400 for cursor recovery, asserting toBeInstanceOf(JobsApiError) and .status === 500 here would actually guard the recover-vs-not classification rather than just confirming some error was stored.
| @@ -604,7 +1345,9 @@ describe('assetsStore - Refactored (Option A)', () => { | |||
| const mockHistory = Array.from({ length: 3 }, (_, i) => | |||
| createMockJobItem(i) | |||
| ) | |||
There was a problem hiding this comment.
suggestion (non-blocking): two boundary gaps in the refresh suite. (1) No test that a refreshHistoryHead merge pushing past MAX_HISTORY_ITEMS trims correctly and evicts from loadedIds (only the offset loadMore path covers trim today). (2) This whole-timeline-replace test asserts only the resulting id list, not that pagination state (cursor / offset / hasMoreHistory) was reset by replaceHistoryWithHeadPage.
| @@ -98,7 +125,7 @@ export async function fetchHistory( | |||
| maxItems: number = 200, | |||
There was a problem hiding this comment.
nitpick (non-blocking): fetchHistory keeps a positional offset param while its sibling fetchHistoryPage moved to { offset?, after? }. The two now have divergent, easily-confused pagination signatures. Optionally align fetchHistory to the same options object.
| const history = await api.getHistory(BATCH_SIZE, { | ||
| offset: historyOffset.value | ||
| }) | ||
| const epoch = historyFetchEpoch |
There was a problem hiding this comment.
nitpick (non-blocking): the historyFetchEpoch guard discards superseded results but does not abort the request, so the 200-job fetch + zod validation still run to completion on rapid resets. Threading an AbortSignal through fetchHistoryPage -> fetchApi would cancel rather than just ignore the orphaned work.
| historyError.value = err | ||
| // Keep existing data when error occurs (consistent with updateHistory) | ||
| if (!historyAssets.value.length) { | ||
| historyAssets.value = [] |
There was a problem hiding this comment.
nitpick (non-blocking): historyAssets and allHistoryItems are aliased to the same array (historyAssets.value = allHistoryItems.value), so reactivity relies on the in-place .push / .splice in mergeHistoryAssets. A future edit swapping those for .filter() / spread would silently break propagation. A computed(() => allHistoryItems.value) (or a comment marking the deliberate alias) would harden this.
ELI-5
The Generated tab shows finished jobs newest-first and loads more as you scroll. Today it asks the server for "items 200–400" (offset paging). If a job finishes while you're scrolling, every item shifts down one slot, so the next page repeats or skips things. Worse, every time a job finished the app threw the whole list away and re-downloaded page one — goodbye scroll position.
Now the server hands back a bookmark (
next_cursor) with each page and we ask for "the page after this bookmark." Bookmarks don't slip when new items arrive, so the scroll walk is drift-free. When a job finishes, we fetch just the newest page and slot the new items in at the top instead of rebuilding everything. If a bookmark goes stale (e.g. the server restarted), we fall back to the old counting method for one page and pick up a fresh bookmark from the response. Old backends that don't hand out bookmarks keep working exactly as before.Summary
Migrates the Generated tab's jobs walk from offset to keyset cursor pagination (
after/next_cursor), now that core (Comfy-Org/ComfyUI#14363) and cloud (BE-885) share one cursor contract onGET /api/jobs.Changes
fetchHistoryPagetakes{ offset?, after? }, sendsafterinstead ofoffsetwhen present, and surfacesnext_cursorfrom the pagination responseoffsetremains the fallback for backends that don't mint cursors400 INVALID_CURSORafter a server restart) is dropped and the page retried once via the offset fallbackfetchJobsRawnow throws on failure instead of fabricating an emptyhasMore: falsepage, so failures are distinguishable from the end of the timeline;api.getHistory/api.getQueuealready catch, the store's error paths now actually run, and the missing-media resolver propagates like its cloud branch already didrefreshHistoryHead()that merge-prepends the head page instead of resetting scroll state; it replaces the list when the page spans the whole timeline (pruning server-side deletions, e.g. cleared history) and restarts the walk when >1 page of new jobs would leave an unfillable gaphasMoreHistoryis server-authoritative (pagination.has_more) instead of inferred from batch lengthfetchHistoryPage/fetchHistory/fetchQueuenow propagate fetch failures instead of returning empty results (all callers audited and handle it)Review Focus
beforeparam, so "fetch newer than X" is not expressible — head-page merge is the prepend mechanism per FE-962refreshHistoryHeadgap/whole-timeline decisions inassetsStore.tsgetAllAssetsByTagwalk inassetService— no overlapping filesLinear: FE-962