From f7547f6d770a2b2cdf4b39297bbef5f35e27c747 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 18:15:20 +0000 Subject: [PATCH 01/16] fix(solid,web): draw an Errored fallback for a Loading's async failure, and adopt it (#4287) Outside a server component, a whose async read failed during a streamed render settled its fragment blank, and the hydrating client rendered the failed boundary from scratch: it asked again and showed the loading fallback. Server: the failing now asks the enclosing for its outcome outside server components too, passing its own id. The Errored answers only when that Loading is all of its content (its resolved markup is the Loading's placeholder): its fallback is drawn into the fragment, or inlined pre-flush, and the error is serialized at the Errored id. Deeper in the content it answers nothing, as before. A rejected fragment's template is marked `e`, and `$dfr` records the nodes it swaps in for it. Client: inside a hydrated Errored's no-claim content run, a runs its content with its data adopted instead of waiting on its fragment, so the Errored holds the client's own error and tracks the content's inputs (#4273). A boundary that claimed its fallback and then renders a rejected fragment fresh clears the nodes the swap put in first, so nothing of the server's outcome is left beside the client's render. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 --- .changeset/errored-async-outcome-4287.md | 6 + documentation/solid-2.0/12-ssr-http.md | 2 +- packages/solid/src/client/hydration.ts | 52 ++++--- packages/solid/src/server/hydration.ts | 16 +- packages/solid/src/server/signals.ts | 32 +++- packages/solid/test/server/ssr-async.spec.ts | 35 ++--- packages/web/src/server.ts | 14 +- .../__artifacts__/async-cond-before-for.json | 2 +- ...async-memo-element-4242-data-streamed.json | 2 +- ...o-element-4242-issue-errored-streamed.json | 2 +- ...element-4242-issue-streamed-reference.json | 2 +- ...sync-memo-element-4242-issue-streamed.json | 2 +- ...emo-element-4242-jsx-errored-streamed.json | 2 +- ...o-element-4242-jsx-streamed-reference.json | 2 +- .../async-memo-element-4242-jsx-streamed.json | 2 +- .../__artifacts__/async-settled-element.json | 2 +- .../async-settled-fragment-root.json | 2 +- .../__artifacts__/bare-client-late-final.json | 2 +- .../__artifacts__/bounded-streamed-text.json | 2 +- ...lient-only-before-suspending-fragment.json | 2 +- .../client-only-before-suspending-route.json | 2 +- .../__artifacts__/cond-component-prop.json | 2 +- .../deferred-hole-before-siblings.json | 2 +- .../deferred-settled-element.json | 2 +- .../__artifacts__/document-shell-styled.json | 2 +- .../dynamic-async-loading-3666-streamed.json | 2 +- .../dynamic-component-parity-streamed.json | 2 +- .../dynamic-fallback-stream.json | 2 +- ...errored-loading-parked-rejection-4287.json | 5 + ...ored-loading-postflush-rejection-4287.json | 5 + .../errored-loading-preflush-rejection.json | 2 +- ...tion-loading-postflush-rejection-4287.json | 5 + .../frame-live-document-loaded.json | 2 +- .../frame-live-document-streamed.json | 2 +- .../frame-live-document-switched.json | 2 +- ...nonlive-document-3666-streamed-loaded.json | 2 +- ...nlive-document-3666-streamed-streamed.json | 2 +- .../__artifacts__/generic-hydration-ab.json | 2 +- .../__artifacts__/generic-hydration-ba.json | 2 +- .../http-primitives-streamed-loading.json | 2 +- .../hybrid-memo-handoff-memo-generator.json | 2 +- .../hybrid-memo-handoff-memo-promise.json | 2 +- .../hybrid-memo-handoff-signal-generator.json | 2 +- .../hybrid-memo-handoff-signal-promise.json | 2 +- .../hybrid-store-handoff-3574-generator.json | 2 +- .../hybrid-store-handoff-3574-promise.json | 2 +- .../late-boundary-after-done.json | 2 +- .../__artifacts__/live-shell-source-3764.json | 6 +- .../loading-fallback-reactive-text.json | 2 +- .../loading-fallback-ternary.json | 2 +- .../__artifacts__/loading-on-boundary.json | 2 +- .../__artifacts__/memo-live-resume.json | 2 +- .../__artifacts__/nav-before-resume.json | 2 +- .../onsettled-store-write-3950-reconcile.json | 2 +- .../onsettled-store-write-3950-splice.json | 2 +- ...d-write-shell-beside-pending-boundary.json | 2 +- .../onsettled-write-show-streamed.json | 2 +- .../__artifacts__/portal-before-siblings.json | 2 +- .../__artifacts__/projection-live-resume.json | 2 +- .../projection-repeat-stream.json | 2 +- ...use-head-stylesheet-streamed-boundary.json | 2 +- .../__artifacts__/welcome-status-loaded.json | 2 +- .../welcome-status-streamed.json | 2 +- .../__artifacts__/write-before-resume.json | 2 +- packages/web/test/harness/scenarios.tsx | 137 ++++++++++++++++-- .../errored-async-outcome-4287.spec.tsx | 88 +++++++++++ .../frame-fragment-error-outcome.spec.tsx | 32 +++- .../server/ssr-async-rejection-3569.spec.tsx | 19 +-- ...ssr-fragment-rejection-stub-batch.spec.tsx | 11 +- packages/web/test/server/ssr-stream.spec.tsx | 29 ++-- 70 files changed, 447 insertions(+), 153 deletions(-) create mode 100644 .changeset/errored-async-outcome-4287.md create mode 100644 packages/web/test/harness/__artifacts__/errored-loading-parked-rejection-4287.json create mode 100644 packages/web/test/harness/__artifacts__/errored-loading-postflush-rejection-4287.json create mode 100644 packages/web/test/harness/__artifacts__/errored-section-loading-postflush-rejection-4287.json create mode 100644 packages/web/test/hydration/errored-async-outcome-4287.spec.tsx diff --git a/.changeset/errored-async-outcome-4287.md b/.changeset/errored-async-outcome-4287.md new file mode 100644 index 000000000..068a76de4 --- /dev/null +++ b/.changeset/errored-async-outcome-4287.md @@ -0,0 +1,6 @@ +--- +"solid-js": patch +"@solidjs/web": patch +--- + +An `` around a `` whose async read fails during a streamed render now shows its fallback in the server HTML outside server components too, and the client adopts it. Before, the boundary's HTML was empty until hydration (neither the loading nor the error fallback), and the hydrating client rendered the failed boundary from scratch: it asked again and showed the loading fallback. Now, when the `` is all of the ``'s content, the server draws the error fallback in its place (in the shell, or in the streamed fragment after the shell) and serializes the error at the ``, so a client that hydrates after the failure arrived claims that markup and adopts its data without asking again, and recovers when the content's inputs change, as for any hydrated failure (#4273). A client that hydrated before the failure arrived renders it once, with nothing of the server's swap left beside it. With markup between the two boundaries the server still leaves the position blank and the client renders the failure (#4287). diff --git a/documentation/solid-2.0/12-ssr-http.md b/documentation/solid-2.0/12-ssr-http.md index f610017c2..17e3daefc 100644 --- a/documentation/solid-2.0/12-ssr-http.md +++ b/documentation/solid-2.0/12-ssr-http.md @@ -193,7 +193,7 @@ renderToStream(code, { onError }); handleServerFunctionRequest(request, { onError }); ``` -- **Called once per error object per `kind` per request, at first sight** (per render when no request is in scope — never once per process: an error object reused across requests is heard, and mapped, by each), with where the failure was met. A direct server-function call that throws during SSR inside an `` is heard twice, as `server-function` and then as `render`, so `kind: "render"` alone tells you a render failed; the wire value is the first one the hook returned. `kind: "render"` — `fallback` (an `` rendered its fallback; `boundary` is its hydration id and `boundaryPath` its component labels — where the error was **met** — while `ownerPath` is where it was **thrown**, the labels up the owner chain it escaped, when the compiler emitted them), `client` (a `` fragment rejected, the client re-renders the subtree), `failed` (nothing contained it; the request fails — including a synchronous throw out of `renderToString` or `renderToStream`'s first pass, reported before it reaches the caller), `serialize` (a hydration value would not serialize and the render went on without it — reported for a render that passed `onError`, as seroval's own `onError` always was). `kind: "server-function"` — `thrown` (the body threw; `functionId`, and `direct: true` for an in-process call during SSR) or `channel` (a rejection or throw escaping through the result graph, the head already committed). `kind: "request"` is always `failed`: a failure that fails the request outside any render or server function the runtime reports (a middleware throw, a setup step), reported by the framework's request handler through `reportRequestFailure(error, event)`. A render failure, synchronous throws included, is reported as `render`, so a handler that catches one and passes it to `reportRequestFailure` adds nothing. It has no per-request tier, so only the ambient hook hears it. `event` is the request, when the failure happened inside one, and always for `kind: "request"`. +- **Called once per error object per `kind` per request, at first sight** (per render when no request is in scope — never once per process: an error object reused across requests is heard, and mapped, by each), with where the failure was met. A direct server-function call that throws during SSR inside an `` is heard twice, as `server-function` and then as `render`, so `kind: "render"` alone tells you a render failed; the wire value is the first one the hook returned. `kind: "render"` — `fallback` (an `` rendered its fallback; `boundary` is its hydration id and `boundaryPath` its component labels — where the error was **met** — while `ownerPath` is where it was **thrown**, the labels up the owner chain it escaped, when the compiler emitted them), `client` (a `` fragment rejected, the client re-renders the subtree — unless the `` is all of an ``'s content: that `` draws its fallback into the fragment, reports `fallback`, and the hydrating client adopts it, [#4287](https://github.com/solidjs/solid/issues/4287)), `failed` (nothing contained it; the request fails — including a synchronous throw out of `renderToString` or `renderToStream`'s first pass, reported before it reaches the caller), `serialize` (a hydration value would not serialize and the render went on without it — reported for a render that passed `onError`, as seroval's own `onError` always was). `kind: "server-function"` — `thrown` (the body threw; `functionId`, and `direct: true` for an in-process call during SSR) or `channel` (a rejection or throw escaping through the result graph, the head already committed). `kind: "request"` is always `failed`: a failure that fails the request outside any render or server function the runtime reports (a middleware throw, a setup step), reported by the framework's request handler through `reportRequestFailure(error, event)`. A render failure, synchronous throws included, is reported as `render`, so a handler that catches one and passes it to `reportRequestFailure` adds nothing. It has no per-request tier, so only the ambient hook hears it. `event` is the request, when the failure happened inside one, and always for `kind: "request"`. - **The return is the wire value**: rendered into the fallback, serialized for hydration, sent as the RPC error. `undefined` leaves the default policy in place (generic outside dev, fidelity in dev). A returned value is the author's intent — like a `wrapInvocation` mapping — and is not sanitized again; `markSafeError` is not needed on it. Ignored for `failed` and `serialize`, which have no wire. One road a mapping does not reach: a rejected async source's serialized rejection is encoded the moment the source rejects, ahead of the boundary, and carries the default policy's value; the hydrating client renders from the boundary's record, which carries the mapping. - **Once means once across roads.** A direct server-function call that throws during SSR is met first by the invocation (`kind: "server-function"`, `direct: true`) and then by the `` that contains it; the boundary reuses the verdict — the same replacement in the fallback and the record — and does not report again. A `` re-pull recurring the same throw is the same object, the same verdict. The observe tier keeps the multi-event picture (an `"invocation"` record _and_ an `SSR_RENDER_ERROR_CONTAINED` finding) for consumers that want it. - **Two tiers, as `wrapInvocation` has.** Ambient: `configureServerErrors({ onError })`, once per process, registered on `globalThis` under a registered symbol so a bundled server build and an instrumented `--import`ed module share it — and the only tier that sees direct in-process calls. Per request, overriding it: `onError` on `renderToStream`/`renderToString`, and on `handleServerFunctionRequest` for the call it dispatches. A throwing hook is reported on the console and treated as silent. With no hook anywhere, a failure that fails the request goes to `console.error` — never silent. A synchronous render throw is the exception: it is rethrown to the caller, whose own handling (or `reportRequestFailure`) decides. diff --git a/packages/solid/src/client/hydration.ts b/packages/solid/src/client/hydration.ts index 73895fbe9..3521fc779 100644 --- a/packages/solid/src/client/hydration.ts +++ b/packages/solid/src/client/hydration.ts @@ -306,6 +306,9 @@ let _snapshotRootOwner: Owner | null = null; // The boundary owner whose resume window is open (null during a root pass, // which claims everything). Reached as `sharedConfig.isClaiming`. let _claimOwner: Owner | null = null; +// The claim window over no owner: a hydrated ``'s content run, +// where nothing claims (hydratedCreateErrorBoundary). +const NO_CLAIM = {} as Owner; function isClaiming(): boolean { if (!_claimOwner) return true; let owner: Owner | null = getOwner(); @@ -1293,9 +1296,7 @@ function hydratedCreateErrorBoundary( // A later run claims nothing either way: claims are read only while // hydrating. () => - _hydratingValue - ? hydrateWindow("", fn, undefined, ((o = getOwner()), {} as Owner)) - : fn(), + _hydratingValue ? hydrateWindow("", fn, undefined, ((o = getOwner()), NO_CLAIM)) : fn(), (e, reset) => ((o = 0), fallback(e, reset)) ); // No fallback yet: the content ran clean. Hold the server's fallback @@ -2853,7 +2854,18 @@ function hydratedCreateLoadingBoundary( // A parked fragment has settled but not swapped: it waits like a pending one. const s = fr && typeof fr === "object" ? (fr.s === 1 && fragmentParked(id) ? 0 : fr.s) : 0; - if (s === 1 && !assetPromise && !fragmentSuperseded(id)) { + // Inside a hydrated ``'s content run, which claims no DOM (the + // markup is the server's fallback for the failure, #4287), the + // fragment is never claimed: the content runs now with its data + // adopted, so the Errored holds the client's own error and tracks its + // inputs (#4273). + const unclaimed = _claimOwner === NO_CLAIM; + // Nothing else consumes a rejected `_fr` promise then — swallow it so + // a rejected fragment (error finalize or a client-hole handoff) doesn't + // surface as an unhandled rejection. + if (s === 2 || unclaimed) (fr as Promise).catch?.(noop); + + if (unclaimed || (s === 1 && !assetPromise && !fragmentSuperseded(id))) { // Fragment already settled and swapped in ($df ran before hydration): // the content is in the DOM, so hydrate straight through. The fallback // only hydrates when it is actually showing — rendering it here would @@ -2895,13 +2907,9 @@ function hydratedCreateLoadingBoundary( if (s === 1 || s === 2) { if (s === 2) { - // Rejected stream fragments swap to an empty template; any outer error fallback - // has to be created as fresh client DOM, not claimed from server markup. - // Nothing else consumes the settled-rejected `_fr` promise once this - // branch takes over — swallow it so a rejected fragment (error - // finalize or a client-hole handoff) doesn't surface as an - // unhandled rejection. - (fr as Promise).catch?.(noop); + // Rejected stream fragments swap to an empty template (or, under a + // hydrated ``, the fallback it claims — above); any outer + // error fallback here is created as fresh client DOM. // Observe: the fresh render is a "recovery" record; prod folds the // branch and `recover` with it. const resumeRejected = IS_OBSERVE @@ -2930,6 +2938,12 @@ function hydratedCreateLoadingBoundary( // for this resume to claim instead of holding it; if the swap already // arrived and was held awaiting a claimant, replay it now. claimFragment(id); + const hy = (globalThis as any)._$HY; + // Revealed ahead of its `_fr` settlement (two scripts on the wire; an + // async entry can run between them, #4074): the fallback is gone and + // the content is in the DOM, so render nothing until the resume + // adopts it. + const revealed = hy.v?.[id]; waitAndResume( fr, shouldHydrate => @@ -2944,18 +2958,18 @@ function hydratedCreateLoadingBoundary( const assets = boundaryAssets(id); assets ? waitAndResume(true, resume, assets) : resume(true); }) - : IS_OBSERVE - ? recover(id, registeredAt, resume) - : resume(false), + : // Rendered fresh. A rejected fragment swapped in over the + // claimed fallback left its nodes — the server's outcome for + // the failure, which `$dfr` lists in the reveal record + // (#4287) — beside the range the fresh render takes: clear + // them first. + (revealed || hy.v?.[id]?.forEach?.((n: ChildNode) => n.remove()), + IS_OBSERVE ? recover(id, registeredAt, resume) : resume(false)), assetPromise, false, fragmentAbort(id) ); - // Revealed ahead of its `_fr` settlement (two scripts on the wire; an - // async entry can run between them, #4074): the fallback is gone and - // the content is in the DOM, so render nothing until the resume - // adopts it. - return (globalThis as any)._$HY.v?.[id] ? undefined : fallback(); + return revealed ? undefined : fallback(); } if (assetPromise && !sharedConfig.has!(id)) { diff --git a/packages/solid/src/server/hydration.ts b/packages/solid/src/server/hydration.ts index f605daa11..94f8719f3 100644 --- a/packages/solid/src/server/hydration.ts +++ b/packages/solid/src/server/hydration.ts @@ -361,8 +361,12 @@ function ssrLoadingBoundary( // What the fragment carries for a failure once it is registered (its // channel owns the routing — see runLoadingPhase). Outside a server - // component: nothing — the blank the client twin renders fresh over, as - // always (`hydratedCreateLoadingBoundary`'s rejected arm). Inside one + // component (asked with this boundary's id, #4287): the fallback of an + // `` this boundary is all the content of — drawn where the client + // twin draws it, its error serialized at the Errored id so the hydrating + // client adopts that markup; otherwise nothing — the blank the client + // twin renders fresh over (`hydratedCreateLoadingBoundary`'s rejected + // arm). Inside one // (frames-rulings 3.3, A0 corollary 4 inward — the position shows what the // SERVER rendered for the outcome, never a blank, never a client-invented // state; there is no twin): the nearest server ``'s fallback for @@ -375,10 +379,10 @@ function ssrLoadingBoundary( // hydration-free scope's; its head and asset registrations drop with the // error, as the error path drops them today. function errorOutcome(err: any): { value?: string; escaped?: { frame?: string } } | undefined { - if (!inServerComponentScope(o as any)) return undefined; - const rendered = parentHandler ? parentHandler(err, true) : undefined; + const scoped = inServerComponentScope(o as any); + const rendered = parentHandler ? parentHandler(err, scoped || id) : undefined; if (typeof rendered === "string") return { value: rendered }; - return { value: plainFallback(), escaped: { frame: ctx.frameId } }; + if (scoped) return { value: plainFallback(), escaped: { frame: ctx.frameId } }; } // Settle the fragment with the failure. The server error hook hears of it // first (the `_fr` rejection and a transport sink's error chunk read the @@ -412,7 +416,7 @@ function ssrLoadingBoundary( (err: any, handler, outcome) => { // The outcome question (see errorOutcome) is a ``'s to pass // up: it owns no fallback for an error. - if (outcome) return handler ? handler(err, true) : undefined; + if (outcome !== undefined) return handler ? handler(err, outcome) : undefined; handledRenderError = err; if (done) { // Once the fragment is registered, its channel owns error routing: diff --git a/packages/solid/src/server/signals.ts b/packages/solid/src/server/signals.ts index 51dce3640..96c4f39b8 100644 --- a/packages/solid/src/server/signals.ts +++ b/packages/solid/src/server/signals.ts @@ -3704,9 +3704,11 @@ export function repeat( * rendered, never a blank): the nearest server `` answers with * its fallback as markup; a `` passes the question up; a handler * that belongs to no server `` answers `undefined` — the error - * escapes the component. + * escapes the component. Outside a server component `outcome` is the + * asking ``'s id, and an `` answers only when that + * Loading is all of its content (#4287). */ -export type BoundaryErrorHandler = (err: any, outcome?: true) => string | undefined | void; +export type BoundaryErrorHandler = (err: any, outcome?: true | string) => string | undefined | void; const ErrorContext: Context = { id: Symbol("ErrorContext"), @@ -3758,7 +3760,7 @@ export function runWithBoundaryErrorContext( onError: ( err: any, parentHandler: BoundaryErrorHandler | null, - outcome?: true + outcome?: true | string ) => string | undefined | void, context?: NonNullable, boundaryId?: string @@ -4108,6 +4110,8 @@ export function createErrorBoundary( // children — re-running would recreate the async work from scratch, which // is pending again on every pass and can never settle (#2809 SSR loop). let pending: { t: string[]; h: Function[]; p: Promise[] } | undefined; + // The content's last resolved markup (see renderOutcome). + let shown: any; // The client boundary is two computeds under `owner`: one runs `fn`, the // next flattens its result. A zero-arg function `fn` hands back — a nested // boundary's accessor, a function child — is unwrapped inside that second @@ -4152,6 +4156,7 @@ export function createErrorBoundary( resolved = resolveIn(() => ctx!.resolve(ctx!.escape(value))); } pending = resolved?.p?.length ? resolved : undefined; + shown = resolved; if (pending) { // Propagate the FINAL classification through the aggregate: with a // client hole in the set, the combined promise can never settle, and @@ -4260,8 +4265,23 @@ export function createErrorBoundary( // the error escapes the component as the frame's own error (the outward // face). A fallback still resolving (an async hole in it) has no finished // markup to answer with and escapes the same way. - const renderOutcome = (err: any): string | undefined => { - if (!ctx || !inServerComponentScope(owner as unknown as SSROwner)) return undefined; + // Outside a server component (`outcome` is the asking ``'s id, + // #4287) this boundary answers only when that Loading's placeholder is + // all of its content: the fallback then lands where the client twin draws + // it, and the error serialized here lets it adopt that markup. Deeper in + // the content it answers nothing — the client renders the failure fresh. + const renderOutcome = (err: any, outcome: true | string): string | undefined => { + let t = shown && shown.t; + if (Array.isArray(t)) t = t[0]; + if ( + !ctx || + (outcome === true + ? !inServerComponentScope(owner as unknown as SSROwner) + : typeof t !== "string" || + !t.startsWith(``; for (let i = 0; i < styles.inline.length; i++) { buffer.write(renderInlineStyle(styles.inline[i], nonce)); } @@ -2401,7 +2407,7 @@ export function renderToStream(code, options = {}) { // before the parser reaches anything written after the link, and $dfr // reads a missing template as a swap that already ran: the reveal // would be dropped and the boundary would stay on its fallback. - buffer.write(``); + buffer.write(tpl); for (const entry of styles.links) { buffer.write( typeof entry === "string" @@ -2410,7 +2416,7 @@ export function renderToStream(code, options = {}) { ); } } else { - buffer.write(``); + buffer.write(tpl); if (!deferActivation) { emitTask(`$df("${key}")`); } diff --git a/packages/web/test/harness/__artifacts__/async-cond-before-for.json b/packages/web/test/harness/__artifacts__/async-cond-before-for.json index 54e9d03ee..ca1c0eacc 100644 --- a/packages/web/test/harness/__artifacts__/async-cond-before-for.json +++ b/packages/web/test/harness/__artifacts__/async-cond-before-for.json @@ -1,5 +1,5 @@ { "name": "async-cond-before-for", "shell": "
loading
", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/async-memo-element-4242-data-streamed.json b/packages/web/test/harness/__artifacts__/async-memo-element-4242-data-streamed.json index 350679124..3e9fe4f2c 100644 --- a/packages/web/test/harness/__artifacts__/async-memo-element-4242-data-streamed.json +++ b/packages/web/test/harness/__artifacts__/async-memo-element-4242-data-streamed.json @@ -1,5 +1,5 @@ { "name": "async-memo-element-4242-data-streamed", "shell": "
loading
", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/async-memo-element-4242-issue-errored-streamed.json b/packages/web/test/harness/__artifacts__/async-memo-element-4242-issue-errored-streamed.json index 068fc19b2..256bf0331 100644 --- a/packages/web/test/harness/__artifacts__/async-memo-element-4242-issue-errored-streamed.json +++ b/packages/web/test/harness/__artifacts__/async-memo-element-4242-issue-errored-streamed.json @@ -1,5 +1,5 @@ { "name": "async-memo-element-4242-issue-errored-streamed", "shell": "outer", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/async-memo-element-4242-issue-streamed-reference.json b/packages/web/test/harness/__artifacts__/async-memo-element-4242-issue-streamed-reference.json index b10575225..bd8804792 100644 --- a/packages/web/test/harness/__artifacts__/async-memo-element-4242-issue-streamed-reference.json +++ b/packages/web/test/harness/__artifacts__/async-memo-element-4242-issue-streamed-reference.json @@ -1,5 +1,5 @@ { "name": "async-memo-element-4242-issue-streamed-reference", "shell": "outer", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/async-memo-element-4242-issue-streamed.json b/packages/web/test/harness/__artifacts__/async-memo-element-4242-issue-streamed.json index 30d373347..34b89d568 100644 --- a/packages/web/test/harness/__artifacts__/async-memo-element-4242-issue-streamed.json +++ b/packages/web/test/harness/__artifacts__/async-memo-element-4242-issue-streamed.json @@ -1,5 +1,5 @@ { "name": "async-memo-element-4242-issue-streamed", "shell": "outer", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/async-memo-element-4242-jsx-errored-streamed.json b/packages/web/test/harness/__artifacts__/async-memo-element-4242-jsx-errored-streamed.json index 823d59d28..31cf10f93 100644 --- a/packages/web/test/harness/__artifacts__/async-memo-element-4242-jsx-errored-streamed.json +++ b/packages/web/test/harness/__artifacts__/async-memo-element-4242-jsx-errored-streamed.json @@ -1,5 +1,5 @@ { "name": "async-memo-element-4242-jsx-errored-streamed", "shell": "
outer
", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/async-memo-element-4242-jsx-streamed-reference.json b/packages/web/test/harness/__artifacts__/async-memo-element-4242-jsx-streamed-reference.json index 494086b3d..806bac710 100644 --- a/packages/web/test/harness/__artifacts__/async-memo-element-4242-jsx-streamed-reference.json +++ b/packages/web/test/harness/__artifacts__/async-memo-element-4242-jsx-streamed-reference.json @@ -1,5 +1,5 @@ { "name": "async-memo-element-4242-jsx-streamed-reference", "shell": "
outer
", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/async-memo-element-4242-jsx-streamed.json b/packages/web/test/harness/__artifacts__/async-memo-element-4242-jsx-streamed.json index a18ad4560..5705c20c0 100644 --- a/packages/web/test/harness/__artifacts__/async-memo-element-4242-jsx-streamed.json +++ b/packages/web/test/harness/__artifacts__/async-memo-element-4242-jsx-streamed.json @@ -1,5 +1,5 @@ { "name": "async-memo-element-4242-jsx-streamed", "shell": "
outer
", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/async-settled-element.json b/packages/web/test/harness/__artifacts__/async-settled-element.json index 4edef7416..59d6ca3d6 100644 --- a/packages/web/test/harness/__artifacts__/async-settled-element.json +++ b/packages/web/test/harness/__artifacts__/async-settled-element.json @@ -1,5 +1,5 @@ { "name": "async-settled-element", "shell": "

loading

", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/async-settled-fragment-root.json b/packages/web/test/harness/__artifacts__/async-settled-fragment-root.json index b3604caa3..24454622c 100644 --- a/packages/web/test/harness/__artifacts__/async-settled-fragment-root.json +++ b/packages/web/test/harness/__artifacts__/async-settled-fragment-root.json @@ -1,5 +1,5 @@ { "name": "async-settled-fragment-root", "shell": "

loading

", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/bare-client-late-final.json b/packages/web/test/harness/__artifacts__/bare-client-late-final.json index 18a7e3691..6dc20e8dc 100644 --- a/packages/web/test/harness/__artifacts__/bare-client-late-final.json +++ b/packages/web/test/harness/__artifacts__/bare-client-late-final.json @@ -1,5 +1,5 @@ { "name": "bare-client-late-final", "shell": "

wait

", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/bounded-streamed-text.json b/packages/web/test/harness/__artifacts__/bounded-streamed-text.json index 9a94984fb..b44464549 100644 --- a/packages/web/test/harness/__artifacts__/bounded-streamed-text.json +++ b/packages/web/test/harness/__artifacts__/bounded-streamed-text.json @@ -1,5 +1,5 @@ { "name": "bounded-streamed-text", "shell": "
before

loading

tail
", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/client-only-before-suspending-fragment.json b/packages/web/test/harness/__artifacts__/client-only-before-suspending-fragment.json index 548552b5a..a8951f8b5 100644 --- a/packages/web/test/harness/__artifacts__/client-only-before-suspending-fragment.json +++ b/packages/web/test/harness/__artifacts__/client-only-before-suspending-fragment.json @@ -1,5 +1,5 @@ { "name": "client-only-before-suspending-fragment", "shell": "wait", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/client-only-before-suspending-route.json b/packages/web/test/harness/__artifacts__/client-only-before-suspending-route.json index 36b974c40..f36bb55f7 100644 --- a/packages/web/test/harness/__artifacts__/client-only-before-suspending-route.json +++ b/packages/web/test/harness/__artifacts__/client-only-before-suspending-route.json @@ -1,5 +1,5 @@ { "name": "client-only-before-suspending-route", "shell": "wait", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/cond-component-prop.json b/packages/web/test/harness/__artifacts__/cond-component-prop.json index b9d173ef7..ed0ae4c20 100644 --- a/packages/web/test/harness/__artifacts__/cond-component-prop.json +++ b/packages/web/test/harness/__artifacts__/cond-component-prop.json @@ -1,5 +1,5 @@ { "name": "cond-component-prop", "shell": "
wait
", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/deferred-hole-before-siblings.json b/packages/web/test/harness/__artifacts__/deferred-hole-before-siblings.json index 1348ebfb2..b84ca9b6c 100644 --- a/packages/web/test/harness/__artifacts__/deferred-hole-before-siblings.json +++ b/packages/web/test/harness/__artifacts__/deferred-hole-before-siblings.json @@ -1,5 +1,5 @@ { "name": "deferred-hole-before-siblings", "shell": "

loading

", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/deferred-settled-element.json b/packages/web/test/harness/__artifacts__/deferred-settled-element.json index 33c04d273..afa4603a7 100644 --- a/packages/web/test/harness/__artifacts__/deferred-settled-element.json +++ b/packages/web/test/harness/__artifacts__/deferred-settled-element.json @@ -1,5 +1,5 @@ { "name": "deferred-settled-element", "shell": "

loading

", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/document-shell-styled.json b/packages/web/test/harness/__artifacts__/document-shell-styled.json index dec281bef..d0686690f 100644 --- a/packages/web/test/harness/__artifacts__/document-shell-styled.json +++ b/packages/web/test/harness/__artifacts__/document-shell-styled.json @@ -1,6 +1,6 @@ { "chunks": [ "

waiting

", - "" + "" ] } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/dynamic-async-loading-3666-streamed.json b/packages/web/test/harness/__artifacts__/dynamic-async-loading-3666-streamed.json index 018f09c60..82b35be4d 100644 --- a/packages/web/test/harness/__artifacts__/dynamic-async-loading-3666-streamed.json +++ b/packages/web/test/harness/__artifacts__/dynamic-async-loading-3666-streamed.json @@ -1,5 +1,5 @@ { "name": "dynamic-async-loading-3666-streamed", "shell": "
Loading…
", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/dynamic-component-parity-streamed.json b/packages/web/test/harness/__artifacts__/dynamic-component-parity-streamed.json index 1bd8e7092..6f8f8b142 100644 --- a/packages/web/test/harness/__artifacts__/dynamic-component-parity-streamed.json +++ b/packages/web/test/harness/__artifacts__/dynamic-component-parity-streamed.json @@ -1,5 +1,5 @@ { "name": "dynamic-component-parity-streamed", "shell": "

client

shell-fallback

", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/dynamic-fallback-stream.json b/packages/web/test/harness/__artifacts__/dynamic-fallback-stream.json index be1e88dde..792396a4a 100644 --- a/packages/web/test/harness/__artifacts__/dynamic-fallback-stream.json +++ b/packages/web/test/harness/__artifacts__/dynamic-fallback-stream.json @@ -1,5 +1,5 @@ { "name": "dynamic-fallback-stream", "shell": "
status preparing 42% done
", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/errored-loading-parked-rejection-4287.json b/packages/web/test/harness/__artifacts__/errored-loading-parked-rejection-4287.json new file mode 100644 index 000000000..a76b96a13 --- /dev/null +++ b/packages/web/test/harness/__artifacts__/errored-loading-parked-rejection-4287.json @@ -0,0 +1,5 @@ +{ + "name": "errored-loading-parked-rejection-4287", + "shell": "
outer loadingtail
", + "rest": "" +} \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/errored-loading-postflush-rejection-4287.json b/packages/web/test/harness/__artifacts__/errored-loading-postflush-rejection-4287.json new file mode 100644 index 000000000..3b03ab1fc --- /dev/null +++ b/packages/web/test/harness/__artifacts__/errored-loading-postflush-rejection-4287.json @@ -0,0 +1,5 @@ +{ + "name": "errored-loading-postflush-rejection-4287", + "shell": "
loadingtail
", + "rest": "" +} \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/errored-loading-preflush-rejection.json b/packages/web/test/harness/__artifacts__/errored-loading-preflush-rejection.json index 78a10d880..18f5834aa 100644 --- a/packages/web/test/harness/__artifacts__/errored-loading-preflush-rejection.json +++ b/packages/web/test/harness/__artifacts__/errored-loading-preflush-rejection.json @@ -1,5 +1,5 @@ { "name": "errored-loading-preflush-rejection", - "shell": "
tail
", + "shell": "

caught

tail
", "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/errored-section-loading-postflush-rejection-4287.json b/packages/web/test/harness/__artifacts__/errored-section-loading-postflush-rejection-4287.json new file mode 100644 index 000000000..e5db53513 --- /dev/null +++ b/packages/web/test/harness/__artifacts__/errored-section-loading-postflush-rejection-4287.json @@ -0,0 +1,5 @@ +{ + "name": "errored-section-loading-postflush-rejection-4287", + "shell": "

title

loading
tail
", + "rest": "" +} \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/frame-live-document-loaded.json b/packages/web/test/harness/__artifacts__/frame-live-document-loaded.json index d3ede6c6b..16b4f6ab1 100644 --- a/packages/web/test/harness/__artifacts__/frame-live-document-loaded.json +++ b/packages/web/test/harness/__artifacts__/frame-live-document-loaded.json @@ -1,5 +1,5 @@ { "name": "frame-live-document-loaded", "shell": "

shell-fallback

", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/frame-live-document-streamed.json b/packages/web/test/harness/__artifacts__/frame-live-document-streamed.json index e6415111d..f23488a91 100644 --- a/packages/web/test/harness/__artifacts__/frame-live-document-streamed.json +++ b/packages/web/test/harness/__artifacts__/frame-live-document-streamed.json @@ -1,5 +1,5 @@ { "name": "frame-live-document-streamed", "shell": "

shell-fallback

", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/frame-live-document-switched.json b/packages/web/test/harness/__artifacts__/frame-live-document-switched.json index d57330f72..a8ac8608f 100644 --- a/packages/web/test/harness/__artifacts__/frame-live-document-switched.json +++ b/packages/web/test/harness/__artifacts__/frame-live-document-switched.json @@ -1,5 +1,5 @@ { "name": "frame-live-document-switched", "shell": "

shell-fallback

", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/frame-nonlive-document-3666-streamed-loaded.json b/packages/web/test/harness/__artifacts__/frame-nonlive-document-3666-streamed-loaded.json index c142f6607..8ed43883d 100644 --- a/packages/web/test/harness/__artifacts__/frame-nonlive-document-3666-streamed-loaded.json +++ b/packages/web/test/harness/__artifacts__/frame-nonlive-document-3666-streamed-loaded.json @@ -1,5 +1,5 @@ { "name": "frame-nonlive-document-3666-streamed-loaded", "shell": "

shell-fallback

", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/frame-nonlive-document-3666-streamed-streamed.json b/packages/web/test/harness/__artifacts__/frame-nonlive-document-3666-streamed-streamed.json index 042a08bfc..076a48a41 100644 --- a/packages/web/test/harness/__artifacts__/frame-nonlive-document-3666-streamed-streamed.json +++ b/packages/web/test/harness/__artifacts__/frame-nonlive-document-3666-streamed-streamed.json @@ -1,5 +1,5 @@ { "name": "frame-nonlive-document-3666-streamed-streamed", "shell": "

shell-fallback

", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/generic-hydration-ab.json b/packages/web/test/harness/__artifacts__/generic-hydration-ab.json index d314c994d..54e9caf93 100644 --- a/packages/web/test/harness/__artifacts__/generic-hydration-ab.json +++ b/packages/web/test/harness/__artifacts__/generic-hydration-ab.json @@ -2,7 +2,7 @@ "shell": "

label:/a

/a

a-loading

b-loading

", "chunks": [ "", - "", + "", "" ] } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/generic-hydration-ba.json b/packages/web/test/harness/__artifacts__/generic-hydration-ba.json index 0544b2389..82a473c5b 100644 --- a/packages/web/test/harness/__artifacts__/generic-hydration-ba.json +++ b/packages/web/test/harness/__artifacts__/generic-hydration-ba.json @@ -2,7 +2,7 @@ "shell": "

label:/a

/a

a-loading

b-loading

", "chunks": [ "", - "", + "", "" ] } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/http-primitives-streamed-loading.json b/packages/web/test/harness/__artifacts__/http-primitives-streamed-loading.json index fd2c289e7..0af0f6cd4 100644 --- a/packages/web/test/harness/__artifacts__/http-primitives-streamed-loading.json +++ b/packages/web/test/harness/__artifacts__/http-primitives-streamed-loading.json @@ -1,5 +1,5 @@ { "name": "http-primitives-streamed-loading", "shell": "

loading

", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/hybrid-memo-handoff-memo-generator.json b/packages/web/test/harness/__artifacts__/hybrid-memo-handoff-memo-generator.json index a8e314f28..f649b9dc6 100644 --- a/packages/web/test/harness/__artifacts__/hybrid-memo-handoff-memo-generator.json +++ b/packages/web/test/harness/__artifacts__/hybrid-memo-handoff-memo-generator.json @@ -1,5 +1,5 @@ { "name": "hybrid-memo-handoff-memo-generator", "shell": "
loading
", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/hybrid-memo-handoff-memo-promise.json b/packages/web/test/harness/__artifacts__/hybrid-memo-handoff-memo-promise.json index 769c4fc9a..e8372e21c 100644 --- a/packages/web/test/harness/__artifacts__/hybrid-memo-handoff-memo-promise.json +++ b/packages/web/test/harness/__artifacts__/hybrid-memo-handoff-memo-promise.json @@ -1,5 +1,5 @@ { "name": "hybrid-memo-handoff-memo-promise", "shell": "
loading
", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/hybrid-memo-handoff-signal-generator.json b/packages/web/test/harness/__artifacts__/hybrid-memo-handoff-signal-generator.json index d362b4273..9eaf541c1 100644 --- a/packages/web/test/harness/__artifacts__/hybrid-memo-handoff-signal-generator.json +++ b/packages/web/test/harness/__artifacts__/hybrid-memo-handoff-signal-generator.json @@ -1,5 +1,5 @@ { "name": "hybrid-memo-handoff-signal-generator", "shell": "
loading
", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/hybrid-memo-handoff-signal-promise.json b/packages/web/test/harness/__artifacts__/hybrid-memo-handoff-signal-promise.json index 75b5aaf48..ead6c3533 100644 --- a/packages/web/test/harness/__artifacts__/hybrid-memo-handoff-signal-promise.json +++ b/packages/web/test/harness/__artifacts__/hybrid-memo-handoff-signal-promise.json @@ -1,5 +1,5 @@ { "name": "hybrid-memo-handoff-signal-promise", "shell": "
loading
", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/hybrid-store-handoff-3574-generator.json b/packages/web/test/harness/__artifacts__/hybrid-store-handoff-3574-generator.json index a0b4279cb..ea30f1d35 100644 --- a/packages/web/test/harness/__artifacts__/hybrid-store-handoff-3574-generator.json +++ b/packages/web/test/harness/__artifacts__/hybrid-store-handoff-3574-generator.json @@ -1,5 +1,5 @@ { "name": "hybrid-store-handoff-3574-generator", "shell": "
loading
", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/hybrid-store-handoff-3574-promise.json b/packages/web/test/harness/__artifacts__/hybrid-store-handoff-3574-promise.json index 214e03825..54087369c 100644 --- a/packages/web/test/harness/__artifacts__/hybrid-store-handoff-3574-promise.json +++ b/packages/web/test/harness/__artifacts__/hybrid-store-handoff-3574-promise.json @@ -1,5 +1,5 @@ { "name": "hybrid-store-handoff-3574-promise", "shell": "
loading
", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/late-boundary-after-done.json b/packages/web/test/harness/__artifacts__/late-boundary-after-done.json index afe358194..2adb6e7e9 100644 --- a/packages/web/test/harness/__artifacts__/late-boundary-after-done.json +++ b/packages/web/test/harness/__artifacts__/late-boundary-after-done.json @@ -1,5 +1,5 @@ { "name": "late-boundary-after-done", "shell": "
lead

waiting

tail
", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/live-shell-source-3764.json b/packages/web/test/harness/__artifacts__/live-shell-source-3764.json index ebd146f00..3e15b7812 100644 --- a/packages/web/test/harness/__artifacts__/live-shell-source-3764.json +++ b/packages/web/test/harness/__artifacts__/live-shell-source-3764.json @@ -2,19 +2,19 @@ "a": { "shell": "", "chunks": [ - "" + "" ] }, "b": { "shell": "", "chunks": [ - "" + "" ] }, "c": { "shell": "

shell

", "chunks": [ - "" + "" ] } } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/loading-fallback-reactive-text.json b/packages/web/test/harness/__artifacts__/loading-fallback-reactive-text.json index 84956f13e..802893719 100644 --- a/packages/web/test/harness/__artifacts__/loading-fallback-reactive-text.json +++ b/packages/web/test/harness/__artifacts__/loading-fallback-reactive-text.json @@ -1,5 +1,5 @@ { "name": "loading-fallback-reactive-text", "shell": "

head

0
", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/loading-fallback-ternary.json b/packages/web/test/harness/__artifacts__/loading-fallback-ternary.json index df4acb39c..f0a455645 100644 --- a/packages/web/test/harness/__artifacts__/loading-fallback-ternary.json +++ b/packages/web/test/harness/__artifacts__/loading-fallback-ternary.json @@ -1,5 +1,5 @@ { "name": "loading-fallback-ternary", "shell": "
wait-a

tail

", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/loading-on-boundary.json b/packages/web/test/harness/__artifacts__/loading-on-boundary.json index 9f67027bf..0a732188e 100644 --- a/packages/web/test/harness/__artifacts__/loading-on-boundary.json +++ b/packages/web/test/harness/__artifacts__/loading-on-boundary.json @@ -1,5 +1,5 @@ { "name": "loading-on-boundary", "shell": "

shell

wait
", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/memo-live-resume.json b/packages/web/test/harness/__artifacts__/memo-live-resume.json index b48db3a29..b8d8e1324 100644 --- a/packages/web/test/harness/__artifacts__/memo-live-resume.json +++ b/packages/web/test/harness/__artifacts__/memo-live-resume.json @@ -1,5 +1,5 @@ { "name": "memo-live-resume", "shell": "

loading

", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/nav-before-resume.json b/packages/web/test/harness/__artifacts__/nav-before-resume.json index 671cbc360..b36ccb3ba 100644 --- a/packages/web/test/harness/__artifacts__/nav-before-resume.json +++ b/packages/web/test/harness/__artifacts__/nav-before-resume.json @@ -1,7 +1,7 @@ { "shell": "

title:/a

label:/a

side-loading

a-loading
", "chunks": [ - "", + "", "" ] } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/onsettled-store-write-3950-reconcile.json b/packages/web/test/harness/__artifacts__/onsettled-store-write-3950-reconcile.json index 402fdfbf8..5b62044e2 100644 --- a/packages/web/test/harness/__artifacts__/onsettled-store-write-3950-reconcile.json +++ b/packages/web/test/harness/__artifacts__/onsettled-store-write-3950-reconcile.json @@ -1,6 +1,6 @@ { "shell": "

pending

", "chunks": [ - "" + "" ] } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/onsettled-store-write-3950-splice.json b/packages/web/test/harness/__artifacts__/onsettled-store-write-3950-splice.json index 402fdfbf8..5b62044e2 100644 --- a/packages/web/test/harness/__artifacts__/onsettled-store-write-3950-splice.json +++ b/packages/web/test/harness/__artifacts__/onsettled-store-write-3950-splice.json @@ -1,6 +1,6 @@ { "shell": "

pending

", "chunks": [ - "" + "" ] } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/onsettled-write-shell-beside-pending-boundary.json b/packages/web/test/harness/__artifacts__/onsettled-write-shell-beside-pending-boundary.json index e12ec3cb2..468624e08 100644 --- a/packages/web/test/harness/__artifacts__/onsettled-write-shell-beside-pending-boundary.json +++ b/packages/web/test/harness/__artifacts__/onsettled-write-shell-beside-pending-boundary.json @@ -1,5 +1,5 @@ { "name": "onsettled-write-shell-beside-pending-boundary", "shell": "

nobody

wait
", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/onsettled-write-show-streamed.json b/packages/web/test/harness/__artifacts__/onsettled-write-show-streamed.json index e39acb4ab..05534fb52 100644 --- a/packages/web/test/harness/__artifacts__/onsettled-write-show-streamed.json +++ b/packages/web/test/harness/__artifacts__/onsettled-write-show-streamed.json @@ -1,5 +1,5 @@ { "name": "onsettled-write-show-streamed", "shell": "
Loading…
", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/portal-before-siblings.json b/packages/web/test/harness/__artifacts__/portal-before-siblings.json index 826e6d21a..d4bc225bc 100644 --- a/packages/web/test/harness/__artifacts__/portal-before-siblings.json +++ b/packages/web/test/harness/__artifacts__/portal-before-siblings.json @@ -1,5 +1,5 @@ { "name": "portal-before-siblings", "shell": "

loading

", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/projection-live-resume.json b/packages/web/test/harness/__artifacts__/projection-live-resume.json index 8aa37db41..393a3649e 100644 --- a/packages/web/test/harness/__artifacts__/projection-live-resume.json +++ b/packages/web/test/harness/__artifacts__/projection-live-resume.json @@ -1,5 +1,5 @@ { "name": "projection-live-resume", "shell": "

loading

", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/projection-repeat-stream.json b/packages/web/test/harness/__artifacts__/projection-repeat-stream.json index 14eb9a35a..67e4ea735 100644 --- a/packages/web/test/harness/__artifacts__/projection-repeat-stream.json +++ b/packages/web/test/harness/__artifacts__/projection-repeat-stream.json @@ -1,5 +1,5 @@ { "name": "projection-repeat-stream", "shell": "

loading

", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/use-head-stylesheet-streamed-boundary.json b/packages/web/test/harness/__artifacts__/use-head-stylesheet-streamed-boundary.json index c0a7480d6..863bd1cf5 100644 --- a/packages/web/test/harness/__artifacts__/use-head-stylesheet-streamed-boundary.json +++ b/packages/web/test/harness/__artifacts__/use-head-stylesheet-streamed-boundary.json @@ -1,5 +1,5 @@ { "name": "use-head-stylesheet-streamed-boundary", "shell": "
lead

waiting

tail 0
", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/welcome-status-loaded.json b/packages/web/test/harness/__artifacts__/welcome-status-loaded.json index 933816757..633044148 100644 --- a/packages/web/test/harness/__artifacts__/welcome-status-loaded.json +++ b/packages/web/test/harness/__artifacts__/welcome-status-loaded.json @@ -1,5 +1,5 @@ { "name": "welcome-status-loaded", "shell": "
1…
", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/welcome-status-streamed.json b/packages/web/test/harness/__artifacts__/welcome-status-streamed.json index ca4333790..0d55150b2 100644 --- a/packages/web/test/harness/__artifacts__/welcome-status-streamed.json +++ b/packages/web/test/harness/__artifacts__/welcome-status-streamed.json @@ -1,5 +1,5 @@ { "name": "welcome-status-streamed", "shell": "
1…
", - "rest": "" + "rest": "" } \ No newline at end of file diff --git a/packages/web/test/harness/__artifacts__/write-before-resume.json b/packages/web/test/harness/__artifacts__/write-before-resume.json index 4a1544f0d..3b839d915 100644 --- a/packages/web/test/harness/__artifacts__/write-before-resume.json +++ b/packages/web/test/harness/__artifacts__/write-before-resume.json @@ -1,6 +1,6 @@ { "shell": "

title:/a

label:/a

side-loading

sync-loading

", "chunks": [ - "" + "" ] } \ No newline at end of file diff --git a/packages/web/test/harness/scenarios.tsx b/packages/web/test/harness/scenarios.tsx index a02d62298..73878cd1c 100644 --- a/packages/web/test/harness/scenarios.tsx +++ b/packages/web/test/harness/scenarios.tsx @@ -62,10 +62,8 @@ export type Scenario = { expectedText: string; /** * Override for the "streamed" replay mode. Needed where the two modes - * legitimately settle to different text — e.g. a live $df swap of a - * REJECTED fragment splices in the rejection template's single-space - * text node (@solidjs/web's rejection channel), which full-page-loaded - * hydration never renders. + * legitimately settle to different text. (A live $df swap of a REJECTED + * fragment no longer leaves its markup beside the fresh render, #4287.) */ expectedTextStreamed?: string; /** @@ -1505,6 +1503,98 @@ function PreflushRejection() { ); } +// --------------------------------------------------------------------------- +// #4287: the same order with the read failing AFTER the shell flushed, the +// async memo outside both boundaries. The server draws the Errored fallback +// into the fragment and serializes the error at the Errored id; the client +// adopts that fallback (no client ask, no loading flash) and, as for any +// hydrated failure (#4273), recovers once the memo's input changes. +let setPostflushKey!: (v: string) => void; +let postflushClientAsks = 0; +// The hydration dry run computes under a mock Promise: count real asks only. +const RealPromise = Promise; +export const postflushClientAskCount = () => postflushClientAsks; +function PostflushRejection() { + const [key, setKey] = createSignal("u7"); + setPostflushKey = setKey; + const user = createMemo(async (): Promise => { + const k = key(); + if (!isServer && Promise === RealPromise) postflushClientAsks++; + await sleep(5); + if (k === "u7") throw new Error("no user 7"); + return k; + }); + return ( +
+

failed: {e().message}

}> + loading}> + user:{user()} + +
+ tail +
+ ); +} + +// The same failure with markup between the Errored and the Loading: the +// Loading's position is not the Errored's, so the server draws nothing there +// (the client twin's fallback replaces the whole
) and the client +// renders the failure fresh, then recovers. +let setNestedKey!: (v: string) => void; +function PostflushRejectionNested() { + const [key, setKey] = createSignal("u7"); + setNestedKey = setKey; + const user = createMemo(async (): Promise => { + const k = key(); + await sleep(5); + if (k === "u7") throw new Error("no user 7"); + return k; + }); + return ( +
+

failed: {e().message}

}> +
+

title

+ loading}> + user:{user()} + +
+
+ tail +
+ ); +} + +// The failure inside an outer still pending when it lands: the +// inner fragment parks in the outer one, which settles later carrying the +// Errored fallback the server drew. +function PostflushRejectionParked() { + const [key] = createSignal("u7"); + const outer = createMemo(async () => { + await sleep(15); + return "outer"; + }); + const user = createMemo(async (): Promise => { + const k = key(); + await sleep(5); + if (k === "u7") throw new Error("no user 7"); + return k; + }); + return ( +
+ outer loading}> + {outer()} +

failed: {e().message}

}> + loading}> + user:{user()} + +
+
+ tail +
+ ); +} + // --------------------------------------------------------------------------- // #3414: a sync throw the SERVER catches, with a zero-arg fallback thunk // (`fallback={() => }`), inside an enclosing boundary. Errored @@ -2817,9 +2907,6 @@ export const scenarios: Scenario[] = [ App: BareClientLateFinal, async: true, expectedText: "widgettail", - // The live rejected swap splices the rejection template's single-space - // text node into the range (see expectedTextStreamed). - expectedTextStreamed: " widgettail", serverText: "wait" }, { @@ -2827,9 +2914,39 @@ export const scenarios: Scenario[] = [ App: PreflushRejection, async: true, expectedText: "caughttail", - // Pre-flush the rejected boundary inlines to an empty region: neither - // fallback reaches the server HTML — only the static sibling. - serverText: "tail" + // Pre-flush the rejected boundary inlines the Errored fallback the + // server drew for it (#4287). + serverText: "caught tail" + }, + { + name: "errored-section-loading-postflush-rejection-4287", + App: PostflushRejectionNested, + async: true, + expectedText: "failed: no user 7tail", + serverText: "title loading tail", + update: () => setNestedKey("u8"), + expectedTextAfterUpdate: "titleuser:u8tail" + }, + { + name: "errored-loading-parked-rejection-4287", + App: PostflushRejectionParked, + async: true, + expectedText: "outerfailed: no user 7tail", + serverText: "outer loading tail failed: no user 7" + // No update pass: hydrated mid-stream, the memo re-asks on the outer + // boundary's resume (as it does on `next`), so the Errored holds the + // server's fallback until reset() rather than tracking the memo. + }, + { + name: "errored-loading-postflush-rejection-4287", + App: PostflushRejection, + async: true, + expectedText: "failed: no user 7tail", + // Shell: the loading fallback; the fragment: the Errored fallback. + serverText: "loading tail failed: no user 7", + update: () => setPostflushKey("u8"), + expectedTextAfterUpdate: "user:u8tail", + stableSelector: "div" }, { name: "errored-thunk-fallback-under-errored", diff --git a/packages/web/test/hydration/errored-async-outcome-4287.spec.tsx b/packages/web/test/hydration/errored-async-outcome-4287.spec.tsx new file mode 100644 index 000000000..f92c89876 --- /dev/null +++ b/packages/web/test/hydration/errored-async-outcome-4287.spec.tsx @@ -0,0 +1,88 @@ +/** + * @jsxImportSource @solidjs/web + * @vitest-environment jsdom + * + * #4287: `` around a `` whose content reads an async memo + * that rejects after the shell flushed. The server draws the Errored + * fallback into the fragment and serializes the error at the Errored id. + * + * - Hydrating after the failure arrived, the client adopts the server's + * fallback node: no loading fallback, no new ask, and (#4273) it recovers + * when the memo's input changes. + * - Hydrating before it arrived, the client renders the failure itself once + * the fragment rejects, with nothing of the server's swap left beside it. + * + * Replays the parity harness artifact (test/server/hydration-harness.spec.tsx). + */ +import { describe, expect, test, vi } from "vitest"; +import { existsSync, readFileSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { flush } from "solid-js"; +import { hydrate } from "@solidjs/web"; +import { scenarios, postflushClientAskCount } from "../harness/scenarios.jsx"; + +const name = "errored-loading-postflush-rejection-4287"; +const file = resolve( + dirname(fileURLToPath(import.meta.url)), + `../harness/__artifacts__/${name}.json` +); +const scenario = scenarios.find(s => s.name === name)!; +const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)); + +function applyChunk(container: HTMLDivElement, chunk: string, first: boolean) { + const scriptRe = /]*)>([\s\S]*?)<\/script>/g; + const scripts = [...chunk.matchAll(scriptRe)].map(m => m[1]); + const stripped = chunk.replace(scriptRe, ""); + if (first) container.innerHTML = stripped; + else container.insertAdjacentHTML("beforeend", stripped); + for (const s of scripts) (0, eval)(s); +} + +async function run(mode: "loaded" | "streamed") { + expect(existsSync(file)).toBe(true); + const { shell, rest } = JSON.parse(readFileSync(file, "utf-8")); + const container = document.createElement("div"); + document.body.appendChild(container); + (globalThis as any)._$HY = { events: [], completed: new WeakSet(), r: {}, fe() {} }; + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + const asksBefore = postflushClientAskCount(); + let dispose: (() => void) | undefined; + try { + applyChunk(container, shell, true); + if (mode === "loaded") applyChunk(container, rest, false); + const serverP = container.querySelector("p"); + dispose = hydrate(() => , container); + flush(); + await sleep(10); + flush(); + if (mode === "streamed") { + expect(container.textContent).toBe("loadingtail"); + applyChunk(container, rest, false); + } + await sleep(30); + flush(); + expect(container.textContent).toBe("failed: no user 7tail"); + expect(container.querySelectorAll("p").length).toBe(1); + if (mode === "loaded") expect(container.querySelector("p")).toBe(serverP); + // The server's answer is used: nothing is asked again on the client. + expect(postflushClientAskCount()).toBe(asksBefore); + + scenario.update!(); + flush(); + await sleep(30); + flush(); + expect(container.textContent).toBe("user:u8tail"); + expect(warn).not.toHaveBeenCalled(); + } finally { + warn.mockRestore(); + dispose?.(); + await sleep(0); + container.remove(); + } +} + +describe("#4287: Errored over a Loading whose read fails after the shell", () => { + test("hydrated after the failure arrived: adopts the server's fallback", () => run("loaded")); + test("hydrated before the failure arrived: shows it once, no ask", () => run("streamed")); +}); diff --git a/packages/web/test/server/frame-fragment-error-outcome.spec.tsx b/packages/web/test/server/frame-fragment-error-outcome.spec.tsx index 4c4c027bd..2c85ac624 100644 --- a/packages/web/test/server/frame-fragment-error-outcome.spec.tsx +++ b/packages/web/test/server/frame-fragment-error-outcome.spec.tsx @@ -18,8 +18,10 @@ * the position keeps the boundary's own markup, its fallback. * * `_fr` still rejects (the client's dev diagnostic, c1), the keyed error - * chunk still rides. Outside a server component nothing changes: the blank - * the client twin renders fresh over. + * chunk still rides. Outside a server component an `` answers only + * for a `` that is all of its content (#4287): its fallback is + * drawn there and the error serialized at its id, for the client twin to + * adopt; deeper in its content the blank the client renders fresh over. */ import { describe, expect, it } from "vitest"; import vm from "node:vm"; @@ -64,7 +66,8 @@ const collectDocument = (code: () => any) => /** Every `` of a document, by key. */ function templates(html: string) { const out: Record = {}; - for (const m of html.matchAll(/