Capture readiness: definitive-failure signal (data-screenshot-failed) - #6064
Open
lukemelia wants to merge 21 commits into
Open
Capture readiness: definitive-failure signal (data-screenshot-failed)#6064lukemelia wants to merge 21 commits into
lukemelia wants to merge 21 commits into
Conversation
PdfDef declares a poster screenshot slot whose capture-only component paints page 1 with pdf.js (pinned CDN build, loaded only inside the capture render — the live viewer stays a native <object> with no PDF engine in the app graph). The component fetches the document bytes, contains page 1 in the capture box at the device scale, and signals readiness through the data-screenshot-pending contract. keyBy 'file-content' skips re-rasterizing on metadata-only edits; useAsThumbnail routes the capture to the fitted cell through the view model's thumbnail seam, with the typed page placeholder as the fallback for uncaptured or capture-errored documents. Part of CS-12526. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…wiring' into cs-12526-pdf-fitted-first-page-poster-poster-slot
…wiring' into cs-12526-pdf-fitted-first-page-poster-poster-slot
…wiring' into cs-12526-pdf-fitted-first-page-poster-poster-slot
…wiring' into cs-12526-pdf-fitted-first-page-poster-poster-slot
…wiring' into cs-12526-pdf-fitted-first-page-poster-poster-slot
…ion-slots-view-model-rewiring' into cs-12526-pdf-fitted-first-page-poster-poster-slot # Conflicts: # packages/realm-server/tests/declared-screenshots-file-test.ts
The poster capture never landed in the environment it runs in: transpiled card code's import() is rewritten into the Boxel loader's fetch-and-transpile pipeline, and pushing a megabyte-scale engine through in-browser transpilation inside the capture's bounded readiness window fails the slot. pdf.js now loads through the browser's own module loader (a Function-wrapped native import the transpiler cannot rewrite), memoized per tab; the cross-origin workerSrc is documented as pdf.js's main-thread fake-worker path, which is the intended mode for a capture render. A corrupt or unreadable document now fails the slot instead of persisting a blank: readiness resolves only after page 1 paints, so the engine's bounded wait fails the capture, no manifest entry lands, and the fitted cell keeps the typed page placeholder — previously the catch resolved readiness and a blank white poster (the slot's default background) outranked the placeholder through the thumbnail seam. The document is destroyed in the finally so captures on a pooled warm tab don't accumulate pdf.js state across renders. Tests: a corrupt-PDF case pins the no-poster posture (no manifest entry, a poster-named screenshotErrors diagnostic, no poster URL in the fitted HTML), and the poster test uses the suite's writeAndSettle helper instead of restating its body. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The poster capture runs on prerender infrastructure, where fetching the engine from a CDN inside the render makes public-network reachability a standing availability dependency of every realm that holds a PDF. pdf.js is now a host dependency served to card code through the virtual network's async shim (the same lazy pattern as bxl/ethers/uuid): the engine's chunk stays out of the host's initial graph and loads only when a capture render imports it, and the wrapper wires a same-origin worker asset so rasterization runs on a real worker instead of pdf.js's main-thread fallback — a cross-origin worker URL is browser-refused. The capture component's import stays dynamic and capture-time, so the live viewer keeps its engine-free native <object> path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The poster capture timed out with the roster intact: the capture ran and data-screenshot-pending never cleared, because a runtime import() of a shimmed bare specifier is not a load path card code can rely on — the loader resolves shims for static imports (fflate in the 3MF extractor is the proven shape), while the dynamic form stalls. The capture component now statically imports a zero-cost sync shim (@cardstack/host/lib/pdfjs-loader) whose function performs the host-side lazy chunk load, keeping the engine's chunk load at the call so the live viewer's engine-free path is unchanged. The pdfjs-dist async shim stays for card code that statically imports the engine. Includes temporary PDFCAP stage logging (console.warn) to be reverted once CI confirms the capture path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e in CI The loader shim id must also resolve for host-bundled copies of card code (test builds import base source directly), so the sync shim now points at a real module under app/lib and the shim registration shares it. Includes a temporary integration probe and temporarily raises the realm-server test runner's prerenderer-chrome log level so the capture page's PDFCAP stage markers reach the CI shard log — both to be reverted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The capture chain completes in under a second on the prerender page, but a tracked update from the modifier's async continuation never flushes a re-render there — the page is settled when the engine starts its bounded wait, and the attribute still read pending at the full timeout. The engine polls raw DOM, so the readiness contract now lives in raw DOM: the wrapper renders data-screenshot-pending statically and the modifier removes the attribute directly once page 1 has painted (or leaves it standing on failure, unchanged posture). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pooled prerender tabs are backgrounded, and Chromium's intensive timer throttling can starve an in-page interval poll past the whole readiness budget — the pending attribute was removed ~100ms into the wait and the next poll never came within 15s, failing slots whose captures had painted. Mutation polling evaluates once at injection (covering an already-clear document) and then on DOM changes, which is exactly when the readiness attribute is removed; it needs no timers, so background throttling cannot starve it. This pairs with the component-side contract that readiness clears by direct DOM mutation — a tracked re-render's flush rides the same throttled timers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The corrupt-PDF test now asserts what the machinery actually promises: no manifest entry lands, the failure is bookkept in diagnostics (this pass's screenshotErrors, or the accumulated capture-failure count once the retry lane's cap excludes the slot), and the declaration-derived injection still embeds the durable URL — it is class-level and cannot know a document is unreadable, so the URL stays an uncaptured 404 the fitted cell's image fallback absorbs. The capture component's comment states that posture instead of claiming the placeholder directly. The temporary stage markers, diagnostics dump, integration probe, and test-runner log level are all reverted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Which pass's diagnostics survive on the row depends on how many retry-lane passes ran before the settle; the durable facts are that no manifest entry and no ledger row ever land for an undecodable document. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A native <object>'s own fetch bypasses service workers, so no Authorization header can ride it and the viewer rendered blank on any non-public realm. In a live render the viewer now fetches the document itself — the host auth service worker injects the realm Authorization header on that GET, the same path that lets <img src> load realm images — and hands the <object> a blob URL. The object is withheld until the fetch settles so a private realm never flashes the plugin's unauthenticated error page, and a failed fetch falls back to the plain URL, which remains the working path for an anonymous visitor on a public realm. Prerender keeps the plain URL with no fetch: the snapshot needs the markup, not the bytes. Blob lifecycle follows the element (revoked on teardown or URL change), and a loaded document is remembered with the URL it belongs to so a model swap can never serve a stale file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…fetch - Card code reaches host library modules as @cardstack/boxel-host/lib/*, the same spelling family as the tools/ shims: shim id renamed, base/host tsconfig paths added, an exact ambient declaration covers the programs that compile base without host sources (named exports declared explicitly — several of those programs have no esModuleInterop), parse.ts maps the prefix for the published CLI, and the @ts-ignore is gone. - card-api's ScreenshotSpec docs now state the component-side readiness contract: clear data-screenshot-pending with removeAttribute from the async continuation, never a tracked re-render, because backgrounded capture tabs throttle the timers a render flush rides. - The PDF capture's catch now names the decode failure via console.warn (piped into prerender logs) so an unreadable document is distinguishable from a hung component; warn, not error, so a per-slot failure never dresses itself as a page fault. - The live PDF viewer shows a loading indicator while its authed fetch is in flight and bounds the fetch at 30s, after which the plain URL takes over — a stalled fetch no longer leaves an empty gray frame. - The blob-handoff host test is named for what it pins, with the uncovered authed service-worker leg called out. - pdfjs-dist moves into the workspace catalog (exact pin) so host and boxel-cli cannot drift apart. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The prerender serializes console args by value; an Error's message and
stack are non-enumerable, so the object form logged as {}.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eadiness A capture-only component that signals readiness via data-screenshot-pending had no way to say "this will never be ready": an undecodable document (corrupt, encrypted, password-protected — an ordinary case, not a corner) left the pending attribute standing and stalled the engine's full pending budget on every render, repeated across the retry lane, each attempt holding the realm's prerender affinity lane. The component now swaps in a data-screenshot-failed attribute instead, and the engine's mutation-polled wait resolves on either signal — failing the slot immediately, with the attribute's value carried into the slot's failure diagnostics so an unreadable file is distinguishable from a hung component. The PDF poster capture adopts the signal on all of its failure paths, and the ScreenshotSpec author docs describe the contract next to the pending one. CS-12902 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lukemelia
marked this pull request as draft
September 9, 2026 21:23
Contributor
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
The behavior change is narrowly scoped, consistently applied across engine/component/docs, and covered by a focused new regression test that would reliably fail (or stall) if the signal were not honored.
Pull request overview
This PR adds a definitive “never ready” signal for capture-only declared-screenshot components, preventing repeated full-timeout waits when content cannot decode (e.g., corrupt/encrypted PDFs) and ensuring the failure reason is propagated into diagnostics.
Changes:
- Prerender engine now treats
data-screenshot-failedas an immediate per-slot failure signal, and includes the attribute’s value in the slot error message. - PDF poster capture swaps
data-screenshot-pending→data-screenshot-failedon all failure paths instead of leaving the slot pending until timeout. - Tests/docs updated to cover and document the new failure signal contract and expected indexing/manifest behavior.
File summaries
| File | Description |
|---|---|
| packages/realm-server/prerender/utils.ts | Recognize data-screenshot-failed during readiness wait and surface its value in failure diagnostics. |
| packages/base/file-formats/pdf-captures.gts | Emit definitive failure signal for missing URL, fetch failure, and decode/render errors. |
| packages/base/card-api.gts | Document the data-screenshot-failed contract alongside the pending readiness signal. |
| packages/realm-server/tests/declared-screenshots-indexing-test.ts | Add regression test ensuring failed slot doesn’t produce manifest/ledger entries and doesn’t block sibling slots; verifies diagnostics message. |
| packages/realm-server/tests/declared-screenshots-file-test.ts | Update corrupt-PDF test commentary to reflect immediate-failure behavior via data-screenshot-failed. |
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
lukemelia
marked this pull request as ready for review
September 11, 2026 04:12
lukemelia
changed the base branch from
cs-12526-pdf-fitted-first-page-poster-poster-slot
to
main
September 11, 2026 04:12
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
CS-12902
A capture-only component that signals readiness via
data-screenshot-pendinghad no way to say "this will never be ready." When content cannot decode — a corrupt, encrypted, or password-protected document, an unreadable video — the only channel was to leave the pending attribute standing, stalling the engine's full pending budget (SCREENSHOT_PENDING_WAIT_MS) on every render, repeated across the retry lane, with each attempt holding the realm's prerender affinity lane. Encrypted and password-protected PDFs make this an ordinary case, not a corner.What this does
packages/realm-server/prerender/utils.ts):waitForScreenshotPendingClearresolves on either signal — pending cleared means ready; adata-screenshot-failedattribute fails the slot immediately. The attribute's value carries the component's stated cause into the slot's failure diagnostics, so an unreadable file is distinguishable from a hung component. One predicate in the existing mutation-polled wait; serves every future pending-signalling capture (video seek, WebGL first frame).packages/base/file-formats/pdf-captures.gts): all failure paths (missing file URL, failed fetch, decode error) now swap pending → failed instead of leaving pending standing. The previousconsole.warncause-naming is replaced by the attribute value, which reaches diagnostics rather than only the console pipe.packages/base/card-api.gts): the failure signal is documented in theScreenshotSpeccontract next to the pending attribute, including the direct-DOM-mutation requirement both signals share.Tests
diagnostics.screenshotErrors. If the failure signal were not honored, this test would stall the full pending budget per render pass across the retry lane — its runtime is itself the regression guard.Stacked on #6029 (the PDF poster PR this follows up).
🤖 Generated with Claude Code