@@ -105,7 +105,7 @@ import {
105105 type StoreTarget ,
106106} from '../reactive-primitives/types' ;
107107import { triggerEffects } from '../reactive-primitives/utils' ;
108- import type { ISsrNode } from '../ssr/ssr-types' ;
108+ import { type ISsrNode } from '../ssr/ssr-types' ;
109109import { runResource , type ResourceDescriptor } from '../use/use-resource' ;
110110import {
111111 Task ,
@@ -124,9 +124,8 @@ import { isServerPlatform } from './platform/platform';
124124import { type QRLInternal } from './qrl/qrl-class' ;
125125import { isQrl } from './qrl/qrl-utils' ;
126126import { ssrNodeDocumentPosition , vnode_documentPosition } from './scheduler-document-position' ;
127- import type { Container , HostElement } from './types' ;
127+ import { SsrNodeFlags , type Container , type HostElement } from './types' ;
128128import { ChoreType } from './util-chore-type' ;
129- import { logWarn } from './utils/log' ;
130129import { QScopedStyle } from './utils/markers' ;
131130import { isPromise , maybeThen , retryOnPromise , safeCall } from './utils/promises' ;
132131import { addComponentStylePrefix } from './utils/scoped-styles' ;
@@ -136,6 +135,8 @@ import { invoke, newInvokeContext } from '../use/use-core';
136135import { findBlockingChore , findBlockingChoreForVisible } from './scheduler-rules' ;
137136import { createNextTick } from './platform/next-tick' ;
138137import { AsyncComputedSignalImpl } from '../reactive-primitives/impl/async-computed-signal-impl' ;
138+ import { isSsrNode } from '../reactive-primitives/subscriber' ;
139+ import { logWarn } from './utils/log' ;
139140
140141// Turn this on to get debug output of what the scheduler is doing.
141142const DEBUG : boolean = false ;
@@ -336,6 +337,31 @@ export const createScheduler = (
336337 addBlockedChore ( chore , blockingChore , blockedChores ) ;
337338 return chore ;
338339 }
340+ if ( isServer && chore . $host$ && isSsrNode ( chore . $host$ ) ) {
341+ const isUpdatable = ! ! ( chore . $host$ . flags & SsrNodeFlags . Updatable ) ;
342+
343+ if ( ! isUpdatable ) {
344+ // We are running on the server.
345+ // On server we can't schedule task for a different host!
346+ // Server is SSR, and therefore scheduling for anything but the current host
347+ // implies that things need to be re-run nad that is not supported because of streaming.
348+ const warningMessage = `A '${ choreTypeToName (
349+ chore . $type$
350+ ) } ' chore was scheduled on a host element that has already been streamed to the client.
351+ This can lead to inconsistencies between Server-Side Rendering (SSR) and Client-Side Rendering (CSR).
352+
353+ Problematic chore:
354+ - Type: ${ choreTypeToName ( chore . $type$ ) }
355+ - Host: ${ chore . $host$ . toString ( ) }
356+ - Nearest element location: ${ chore . $host$ . currentFile }
357+
358+ This is often caused by modifying a signal in an already rendered component during SSR.` ;
359+ logWarn ( warningMessage ) ;
360+ DEBUG &&
361+ debugTrace ( 'schedule.SKIPPED host is not updatable' , chore , choreQueue , blockedChores ) ;
362+ return chore ;
363+ }
364+ }
339365 chore = sortedInsert (
340366 choreQueue ,
341367 chore ,
@@ -710,15 +736,6 @@ export const createScheduler = (
710736 } else {
711737 assertFalse ( vnode_isVNode ( aHost ) , 'expected aHost to be SSRNode but it is a VNode' ) ;
712738 assertFalse ( vnode_isVNode ( bHost ) , 'expected bHost to be SSRNode but it is a VNode' ) ;
713- // we are running on the server.
714- // On server we can't schedule task for a different host!
715- // Server is SSR, and therefore scheduling for anything but the current host
716- // implies that things need to be re-run nad that is not supported because of streaming.
717- const errorMessage = `SERVER: during HTML streaming, re-running tasks on a different host is not allowed.
718- You are attempting to change a state that has already been streamed to the client.
719- This can lead to inconsistencies between Server-Side Rendering (SSR) and Client-Side Rendering (CSR).
720- Problematic Node: ${ aHost . toString ( ) } ` ;
721- logWarn ( errorMessage ) ;
722739 const hostDiff = ssrNodeDocumentPosition ( aHost as ISsrNode , bHost as ISsrNode ) ;
723740 if ( hostDiff !== 0 ) {
724741 return hostDiff ;
@@ -851,6 +868,25 @@ export function addBlockedChore(
851868 blockedChores . add ( blockedChore ) ;
852869}
853870
871+ function choreTypeToName ( type : ChoreType ) : string {
872+ return (
873+ (
874+ {
875+ [ ChoreType . QRL_RESOLVE ] : 'Resolve QRL' ,
876+ [ ChoreType . RUN_QRL ] : 'Run QRL' ,
877+ [ ChoreType . TASK ] : 'Task' ,
878+ [ ChoreType . NODE_DIFF ] : 'Changes diffing' ,
879+ [ ChoreType . NODE_PROP ] : 'Updating node property' ,
880+ [ ChoreType . COMPONENT ] : 'Component' ,
881+ [ ChoreType . RECOMPUTE_AND_SCHEDULE_EFFECTS ] : 'Signal recompute' ,
882+ [ ChoreType . VISIBLE ] : 'Visible' ,
883+ [ ChoreType . CLEANUP_VISIBLE ] : 'Cleanup visible' ,
884+ [ ChoreType . WAIT_FOR_QUEUE ] : 'Wait for queue' ,
885+ } as Record < ChoreType , string >
886+ ) [ type ] || 'Unknown: ' + type
887+ ) ;
888+ }
889+
854890function debugChoreTypeToString ( type : ChoreType ) : string {
855891 return (
856892 (
0 commit comments