@ -3,7 +3,7 @@
/** @import { Csp, RenderOutput, SyncRenderOutput, Sha256Source } from '../../server/public.js' */
/** @import { Csp, RenderOutput, SyncRenderOutput, Sha256Source } from '../../server/public.js' */
/** @import { MaybePromise } from '#shared' */
/** @import { MaybePromise } from '#shared' */
import { async _mode _flag } from '../flags/index.js' ;
import { async _mode _flag } from '../flags/index.js' ;
import { abort } from './abort-signal .js';
import { STALE_REACTION } from '../client/constants .js';
import { pop , push , set _ssr _context , ssr _context } from './context.js' ;
import { pop , push , set _ssr _context , ssr _context } from './context.js' ;
import * as e from './errors.js' ;
import * as e from './errors.js' ;
import * as w from './warnings.js' ;
import * as w from './warnings.js' ;
@ -180,7 +180,7 @@ export class Renderer {
// prevent unhandled rejections, and attach the promise to the renderer instance
// prevent unhandled rejections, and attach the promise to the renderer instance
// so that rejections correctly cause rendering to fail
// so that rejections correctly cause rendering to fail
promise . catch ( noop ) ;
promise . catch ( noop ) ;
this . promise = promise ;
this . promise = this . global . track ( promise ) ;
return promises ;
return promises ;
}
}
@ -225,7 +225,7 @@ export class Renderer {
e . await _invalid ( ) ;
e . await _invalid ( ) ;
}
}
child . promise = result;
child . promise = child. global . track ( result) ;
}
}
return child ;
return child ;
@ -273,7 +273,7 @@ export class Renderer {
e . await _invalid ( ) ;
e . await _invalid ( ) ;
}
}
result . catch ( noop ) ;
result . catch ( noop ) ;
child . promise = result;
child . promise = child. global . track ( result) ;
}
}
} catch ( error ) {
} catch ( error ) {
// synchronous errors are handled here, async errors will be handled in #collect_content_async
// synchronous errors are handled here, async errors will be handled in #collect_content_async
@ -293,12 +293,14 @@ export class Renderer {
e . await _invalid ( ) ;
e . await _invalid ( ) ;
}
}
child . promise = /** @type {Promise<unknown>} */ ( result ) . then ( ( transformed ) => {
child . promise = child . global . track (
set _ssr _context ( parent _context ) ;
/** @type {Promise<unknown>} */ ( result ) . then ( ( transformed ) => {
child . # out . push ( Renderer . # serialize _failed _boundary ( transformed ) ) ;
set _ssr _context ( parent _context ) ;
failed _snippet ( child , transformed , noop ) ;
child . # out . push ( Renderer . # serialize _failed _boundary ( transformed ) ) ;
child . # out . push ( BLOCK _CLOSE ) ;
failed _snippet ( child , transformed , noop ) ;
} ) ;
child . # out . push ( BLOCK _CLOSE ) ;
} )
) ;
child . promise . catch ( noop ) ;
child . promise . catch ( noop ) ;
} else {
} else {
child . # out . push ( Renderer . # serialize _failed _boundary ( result ) ) ;
child . # out . push ( Renderer . # serialize _failed _boundary ( result ) ) ;
@ -317,8 +319,11 @@ export class Renderer {
* /
* /
component ( fn , component _fn ) {
component ( fn , component _fn ) {
push ( component _fn ) ;
push ( component _fn ) ;
const child = this . child ( fn ) ;
// mark before running so `onDestroy` callbacks are still collected if `fn` throws
child . # is _component _body = true ;
this . child ( ( renderer ) => {
renderer . # is _component _body = true ;
return fn ( renderer ) ;
} ) ;
pop ( ) ;
pop ( ) ;
}
}
@ -626,6 +631,49 @@ export class Renderer {
}
}
}
}
/ * *
* Runs every ` onDestroy ` callback in this renderer tree . On a failed render ,
* cleanup errors are suppressed so they do not mask the render error .
* @ param { boolean } suppress _errors
* /
# run _on _destroy ( suppress _errors ) {
let first _error ;
let has _error = false ;
for ( const cleanup of this . # collect _on _destroy ( ) ) {
try {
cleanup ( ) ;
} catch ( error ) {
if ( ! suppress _errors && ! has _error ) {
first _error = error ;
has _error = true ;
}
}
}
if ( has _error ) throw first _error ;
}
/ * *
* @ param { 'sync' | 'async' } mode
* @ param { { idPrefix ? : string ; csp ? : Csp ; transformError ? : ( error : unknown ) => unknown } } options
* @ returns { Renderer }
* /
static # create ( mode , options ) {
if ( options . idPrefix ? . includes ( '--' ) ) {
e . invalid _id _prefix ( ) ;
}
return new Renderer (
new SSRState (
mode ,
options . idPrefix ? options . idPrefix + '-' : '' ,
options . csp ,
options . transformError
)
) ;
}
/ * *
/ * *
* Render a component . Throws if any of the children are performing asynchronous work .
* Render a component . Throws if any of the children are performing asynchronous work .
*
*
@ -636,13 +684,27 @@ export class Renderer {
* /
* /
static # render ( component , options ) {
static # render ( component , options ) {
var previous _context = ssr _context ;
var previous _context = ssr _context ;
const renderer = Renderer . # create ( 'sync' , options ) ;
/** @type {AccumulatedContent | undefined} */
let result ;
let render _error ;
let failed = false ;
try {
try {
const renderer = Renderer . # open _render ( 'sync' , component , options ) ;
try {
Renderer . # open _render ( renderer , component , options ) ;
result = Renderer . # close _render ( renderer . # collect _content ( ) , renderer ) ;
} catch ( error ) {
render _error = error ;
failed = true ;
}
renderer . # run _on _destroy ( failed ) ;
if ( failed ) throw render _error ;
const content = renderer . # collect _content ( ) ;
return /** @type {AccumulatedContent} */ ( result ) ;
return Renderer . # close _render ( content , renderer ) ;
} finally {
} finally {
abort ( ) ;
renderer. global . abort( ) ;
set _ssr _context ( previous _context ) ;
set _ssr _context ( previous _context ) ;
}
}
}
}
@ -657,18 +719,35 @@ export class Renderer {
* /
* /
static async # render _async ( component , options ) {
static async # render _async ( component , options ) {
const previous _context = ssr _context ;
const previous _context = ssr _context ;
const renderer = Renderer . # create ( 'async' , options ) ;
/** @type {(AccumulatedContent & { hashes: { script: Sha256Source[] } }) | undefined} */
let result ;
let render _error ;
let failed = false ;
try {
try {
const renderer = Renderer . # open _render ( 'async' , component , options ) ;
try {
const content = await renderer . # collect _content _async ( ) ;
Renderer . # open _render ( renderer , component , options ) ;
const hydratables = await renderer . # collect _hydratables ( ) ;
const content = await renderer . # collect _content _async ( ) ;
if ( hydratables !== null ) {
const hydratables = await renderer . # collect _hydratables ( ) ;
content . head = hydratables + content . head ;
if ( hydratables !== null ) {
content . head = hydratables + content . head ;
}
result = Renderer . # close _render ( content , renderer ) ;
} catch ( error ) {
render _error = error ;
failed = true ;
renderer . global . abort ( ) ;
await renderer . global . settle ( ) ;
}
}
return Renderer . # close _render ( content , renderer ) ;
renderer . # run _on _destroy ( failed ) ;
if ( failed ) throw render _error ;
return /** @type {AccumulatedContent & { hashes: { script: Sha256Source[] } }} */ ( result ) ;
} finally {
} finally {
set _ssr _context ( previous _context ) ;
set _ssr _context ( previous _context ) ;
abort ( ) ;
renderer. global . abort( ) ;
}
}
}
}
@ -760,28 +839,15 @@ export class Renderer {
/ * *
/ * *
* @ template { Record < string , any > } Props
* @ template { Record < string , any > } Props
* @ param { 'sync' | 'async' } mode
* @ param { Renderer } renderer
* @ param { import ( 'svelte' ) . Component < Props > } component
* @ param { import ( 'svelte' ) . Component < Props > } component
* @ param { { props ? : Omit < Props , '$$slots' | '$$events' > ; context ? : Map < any , any > ; idPrefix ? : string ; csp ? : Csp ; transformError ? : ( error : unknown ) => unknown } } options
* @ param { { props ? : Omit < Props , '$$slots' | '$$events' > ; context ? : Map < any , any > ; idPrefix ? : string ; csp ? : Csp ; transformError ? : ( error : unknown ) => unknown } } options
* @ returns { Renderer }
* @ returns { void }
* /
* /
static # open _render ( mode , component , options ) {
static # open _render ( renderer , component , options ) {
if ( options . idPrefix ? . includes ( '--' ) ) {
e . invalid _id _prefix ( ) ;
}
var previous _context = ssr _context ;
var previous _context = ssr _context ;
try {
try {
const renderer = new Renderer (
new SSRState (
mode ,
options . idPrefix ? options . idPrefix + '-' : '' ,
options . csp ,
options . transformError
)
) ;
/** @type {SSRContext} */
/** @type {SSRContext} */
const context = { p : null , c : options . context ? ? null , r : renderer } ;
const context = { p : null , c : options . context ? ? null , r : renderer } ;
set _ssr _context ( context ) ;
set _ssr _context ( context ) ;
@ -790,8 +856,6 @@ export class Renderer {
// @ts-expect-error
// @ts-expect-error
component ( renderer , options . props ? ? { } ) ;
component ( renderer , options . props ? ? { } ) ;
renderer . push ( BLOCK _CLOSE ) ;
renderer . push ( BLOCK _CLOSE ) ;
return renderer ;
} finally {
} finally {
set _ssr _context ( previous _context ) ;
set _ssr _context ( previous _context ) ;
}
}
@ -803,10 +867,6 @@ export class Renderer {
* @ returns { AccumulatedContent & { hashes : { script : Sha256Source [ ] } } }
* @ returns { AccumulatedContent & { hashes : { script : Sha256Source [ ] } } }
* /
* /
static # close _render ( content , renderer ) {
static # close _render ( content , renderer ) {
for ( const cleanup of renderer . # collect _on _destroy ( ) ) {
cleanup ( ) ;
}
let head = content . head + renderer . global . get _title ( ) ;
let head = content . head + renderer . global . get _title ( ) ;
let body = content . body ;
let body = content . body ;
@ -890,6 +950,14 @@ export class SSRState {
/** @readonly @type {Set<{ hash: string; code: string }>} */
/** @readonly @type {Set<{ hash: string; code: string }>} */
css = new Set ( ) ;
css = new Set ( ) ;
/** @type {Set<Promise<unknown>>} */
# pending = new Set ( ) ;
/** @type {AbortController | null} */
# controller = null ;
# aborted = false ;
/ * *
/ * *
* ` transformError ` passed to ` render ` . Called when an error boundary catches an error .
* ` transformError ` passed to ` render ` . Called when an error boundary catches an error .
* Throws by default if unset in ` render ` .
* Throws by default if unset in ` render ` .
@ -920,6 +988,38 @@ export class SSRState {
this . uid = ( ) => ` ${ id _prefix } s ${ uid ++ } ` ;
this . uid = ( ) => ` ${ id _prefix } s ${ uid ++ } ` ;
}
}
/ * *
* @ template T
* @ param { Promise < T > } promise
* @ returns { Promise < T > }
* /
track ( promise ) {
this . # pending . add ( promise ) ;
promise . then (
( ) => this . # pending . delete ( promise ) ,
( ) => this . # pending . delete ( promise )
) ;
return promise ;
}
async settle ( ) {
while ( this . # pending . size > 0 ) {
await Promise . allSettled ( [ ... this . # pending ] ) ;
}
}
abort ( ) {
if ( this . # aborted ) return ;
this . # aborted = true ;
this . # controller ? . abort ( STALE _REACTION ) ;
}
get _abort _signal ( ) {
const controller = ( this . # controller ? ? = new AbortController ( ) ) ;
if ( this . # aborted ) controller . abort ( STALE _REACTION ) ;
return controller . signal ;
}
get _title ( ) {
get _title ( ) {
return this . # title . value ;
return this . # title . value ;
}
}