Skip to content

Capture readiness: definitive-failure signal (data-screenshot-failed) - #6064

Open
lukemelia wants to merge 21 commits into
mainfrom
cs-12902-capture-readiness-add-a-definitive-failure-signal-data
Open

Capture readiness: definitive-failure signal (data-screenshot-failed)#6064
lukemelia wants to merge 21 commits into
mainfrom
cs-12902-capture-readiness-add-a-definitive-failure-signal-data

Conversation

@lukemelia

Copy link
Copy Markdown
Contributor

CS-12902

A capture-only component that signals readiness via data-screenshot-pending had 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

  • Engine (packages/realm-server/prerender/utils.ts): waitForScreenshotPendingClear resolves on either signal — pending cleared means ready; a data-screenshot-failed attribute 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).
  • PDF poster capture (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 previous console.warn cause-naming is replaced by the attribute value, which reaches diagnostics rather than only the console pipe.
  • Author docs (packages/base/card-api.gts): the failure signal is documented in the ScreenshotSpec contract next to the pending attribute, including the direct-DOM-mutation requirement both signals share.

Tests

  • New indexing test: a fixture capture-only component swaps pending → failed; its slot fails with no manifest entry and no ledger row, the sibling slot on the same card still captures, and the component's stated cause lands in 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.
  • The corrupt-PDF file test now exercises the immediate-failure path end-to-end (pdf.js decode failure → failed attribute mid-wait) with the same assertions as before.

Stacked on #6029 (the PDF poster PR this follows up).

🤖 Generated with Claude Code

lukemelia and others added 21 commits September 4, 2026 22:07
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
lukemelia marked this pull request as draft September 9, 2026 21:23
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Host Test Results

    1 files      1 suites   2h 28m 55s ⏱️
4 642 tests 4 628 ✅ 14 💤 0 ❌
4 657 runs  4 643 ✅ 14 💤 0 ❌

Results for commit 355cc56.

Realm Server Test Results

    1 files    203 suites   1h 18m 41s ⏱️
2 653 tests 2 653 ✅ 0 💤 0 ❌
2 692 runs  2 692 ✅ 0 💤 0 ❌

Results for commit 355cc56.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 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-failed as an immediate per-slot failure signal, and includes the attribute’s value in the slot error message.
  • PDF poster capture swaps data-screenshot-pendingdata-screenshot-failed on 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
lukemelia marked this pull request as ready for review September 11, 2026 04:12
@lukemelia
lukemelia requested review from a team and habdelra September 11, 2026 04:12
@lukemelia
lukemelia changed the base branch from cs-12526-pdf-fitted-first-page-poster-poster-slot to main September 11, 2026 04:12
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.

2 participants