Skip to content

fix(items): retry failed model loads, settle skipped items, keep exports clean#480

Merged
wass08 merged 2 commits into
mainfrom
fix/bake-item-load-resilience
Jul 9, 2026
Merged

fix(items): retry failed model loads, settle skipped items, keep exports clean#480
wass08 merged 2 commits into
mainfrom
fix/bake-item-load-resilience

Conversation

@wass08

@wass08 wass08 commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Makes item model loading resilient for both interactive use and headless bakes. Today a transient storage failure (observed in prod: Supabase 504s under the bake page's ~200-concurrent-request asset burst) permanently breaks an item for the session — drei's useGLTF caches the rejection — and worse, the red debug-box fallback gets baked into the exported GLB (observed in a prod artifact: 24-vert boxes with the red translucent material where 4 kettles should be).

  • Bounded retries (1s/3s): clear the useGLTF cache entry and re-mount via a new resetKey on ErrorBoundary. The retry timer is effect-owned, keyed on failure count — StrictMode's synthetic unmount/remount re-arms it (an onError-scheduled timer dies silently in dev, verified).
  • Exhausted retries → item settles as skipped: the debug box renders nothing while isExporting, and the failure is recorded in useViewer.itemLoadFailures (nodeId → url) so a bake host can persist which items are missing.
  • Scene-ready waits for item content: ItemSystem now holds the dirty mark until the item settles (model mounted / terminally failed / never expected) instead of clearing at group registration — closing a race where a bake could export loading placeholders; the loading placeholder also hides during exports.

How to test

  1. bun dev, open a scene with items — loading placeholders, hover/selection, and slot materials behave as before.
  2. Simulate a dead asset (devtools → block an item's model URL, reload): after ~5s (2 retries) the item shows the red debug box; the scene stays interactive.
  3. Bake locally with the URL blocked: the exported GLB contains no debug box for that item and the bake completes; unblock, re-bake: item present.

Verified against a 200+-item prod scene: permanent 504 → exactly 3 fetch attempts then a clean skip; 504-once → retry heals, artifact byte-equivalent to intact; no-failure runs byte-identical (demo_1).

Screenshots / screen recording

N/A — failure-path behavior; exported-GLB assertions above are the evidence.

Checklist

  • I've tested this locally with bun dev
  • My code follows the existing code style (run bun check to verify)
  • I've updated relevant documentation (if applicable)
  • This PR targets the main branch

🤖 Generated with Claude Code


Note

Medium Risk
Changes item load lifecycle and scene-ready gating used by headless GLB bakes; incorrect settlement timing could delay exports or omit items, but happy-path behavior is intended to stay byte-identical.

Overview
Item GLB loading is now resilient to transient CDN/storage failures and headless bakes no longer capture debug or loading placeholders.

ModelWithRetry wraps item loads with bounded retries (1s / 3s): on failure it clears drei's useGLTF cache, bumps an ErrorBoundary resetKey, and re-mounts. After retries are exhausted the item settles as skipped—the red debug box stays in dev but renders nothing when isExporting; failures are recorded in useViewer.itemLoadFailures (nodeId → URL) for bake metadata.

Scene-ready / bake timing: ItemRenderer sets userData.itemModelSettled when the model mounts, fails terminally, or isn't needed (e.g. room-clear preview). ItemSystem only clears the dirty mark once settled, so scene-ready and exports wait for real geometry instead of grey loading boxes.

Export hygiene: PreviewModel and BrokenItemFallback return null during export. ErrorBoundary gains onError and resetKey to support the retry flow.

Reviewed by Cursor Bugbot for commit 2b032cc. Bugbot is set up for automated code reviews on this repo. Configure here.

…rts clean

A transient storage failure (observed: Supabase 504s under the bake page's
~200-concurrent-request burst) permanently broke an item for the whole
session: drei's useGLTF caches the rejected load by URL, the per-item
ErrorBoundary swallowed it, and the red debug-box fallback was BAKED into
the exported GLB (observed in a prod artifact). Meanwhile ItemSystem
cleared the dirty mark at group registration — before the model resolved —
so scene-ready could fire while GLBs were still loading, risking exported
placeholder geometry.

- ModelWithRetry: bounded retries (1s/3s) that clear the useGLTF cache
  entry and re-mount via the boundary's new resetKey. The timer is owned
  by an effect keyed on the failure count, so StrictMode's synthetic
  unmount/remount re-arms it instead of silently discarding it (the
  naive onError-scheduled timer died exactly that way in dev).
- Exhausted retries settle the item as SKIPPED: the debug box renders
  nothing during exports, and the failure lands in
  useViewer.itemLoadFailures (nodeId -> url) so a bake host can persist
  which items are missing from the artifact.
- ItemSystem holds the dirty mark until the item settles (model mounted,
  terminally failed, or never expected) — scene-ready now genuinely waits
  for item content; loading placeholders also hide during exports.
- ErrorBoundary: onError + resetKey props.

Verified against a 200+-item prod scene locally: permanent 504 -> exactly
3 fetch attempts, bake completes without the item and without debug
boxes; 504-once -> retry heals, artifact byte-equivalent to the intact
run; no-failure runs unchanged (demo_1 byte-identical).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6a7d9e9. Configure here.

// isn't "built": clearing then would let scene-ready fire while GLBs are
// still downloading and a bake would export loading placeholders.
const settled = (mesh.userData as { itemModelSettled?: boolean }).itemModelSettled
if (!settled) return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scene ready beats item settlement

High Severity

ItemSystem now keeps items in dirtyNodes until itemModelSettled, but SceneReadyTracker still marks the scene ready after SCENE_READY_MAX_WAIT_FRAMES even when hasPendingSceneBuildWork() remains true. A bake that follows that signal can export while GLBs are still loading; with isExporting, item placeholders render nothing, so those items can appear as empty groups in the GLB.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6a7d9e9. Configure here.

Comment thread packages/nodes/src/item/renderer.tsx
Bugbot: after a terminal load failure, swapping the item's model kept the
stale failures/epoch state and the settled flag — the new URL never even
attempted to load. ModelWithRetry is now keyed by asset src (clean retry
budget per URL) and un-settles the item on mount so the replacement load
is awaited by ItemSystem/scene-ready too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@wass08

wass08 commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed both Bugbot findings:

  • Stale settled flag on URL change — fixed in the follow-up commit: ModelWithRetry is now keyed by node.asset.src (fresh retry budget per URL) and un-settles the item on mount, so a model swap is loaded and awaited like a first load. Re-validated the 504 scenarios (permanent → 3 attempts + clean skip; transient → heals).
  • Scene ready beats item settlement — the 180-frame cap firing with unsettled items is the pre-existing backstop semantics (better than hanging forever; before this PR the same capped export shipped placeholder boxes, now it ships nothing for those items). The silent part is being closed on the consumer side: the bake page will report any still-dirty item nodes at export time into the bake metadata alongside itemLoadFailures (pascalorg/private-editor#198), so a cap-forced incomplete artifact is always queryable.

@wass08 wass08 merged commit feeabf4 into main Jul 9, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant