@@ -105,7 +105,7 @@ import {
105
105
type StoreTarget ,
106
106
} from '../reactive-primitives/types' ;
107
107
import { triggerEffects } from '../reactive-primitives/utils' ;
108
- import type { ISsrNode } from '../ssr/ssr-types' ;
108
+ import { type ISsrNode } from '../ssr/ssr-types' ;
109
109
import { runResource , type ResourceDescriptor } from '../use/use-resource' ;
110
110
import {
111
111
Task ,
@@ -124,9 +124,8 @@ import { isServerPlatform } from './platform/platform';
124
124
import { type QRLInternal } from './qrl/qrl-class' ;
125
125
import { isQrl } from './qrl/qrl-utils' ;
126
126
import { ssrNodeDocumentPosition , vnode_documentPosition } from './scheduler-document-position' ;
127
- import type { Container , HostElement } from './types' ;
127
+ import { SsrNodeFlags , type Container , type HostElement } from './types' ;
128
128
import { ChoreType } from './util-chore-type' ;
129
- import { logWarn } from './utils/log' ;
130
129
import { QScopedStyle } from './utils/markers' ;
131
130
import { isPromise , maybeThen , retryOnPromise , safeCall } from './utils/promises' ;
132
131
import { addComponentStylePrefix } from './utils/scoped-styles' ;
@@ -136,6 +135,8 @@ import { invoke, newInvokeContext } from '../use/use-core';
136
135
import { findBlockingChore , findBlockingChoreForVisible } from './scheduler-rules' ;
137
136
import { createNextTick } from './platform/next-tick' ;
138
137
import { AsyncComputedSignalImpl } from '../reactive-primitives/impl/async-computed-signal-impl' ;
138
+ import { isSsrNode } from '../reactive-primitives/subscriber' ;
139
+ import { logWarn } from './utils/log' ;
139
140
140
141
// Turn this on to get debug output of what the scheduler is doing.
141
142
const DEBUG : boolean = false ;
@@ -336,6 +337,31 @@ export const createScheduler = (
336
337
addBlockedChore ( chore , blockingChore , blockedChores ) ;
337
338
return chore ;
338
339
}
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
+ }
339
365
chore = sortedInsert (
340
366
choreQueue ,
341
367
chore ,
@@ -710,15 +736,6 @@ export const createScheduler = (
710
736
} else {
711
737
assertFalse ( vnode_isVNode ( aHost ) , 'expected aHost to be SSRNode but it is a VNode' ) ;
712
738
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 ) ;
722
739
const hostDiff = ssrNodeDocumentPosition ( aHost as ISsrNode , bHost as ISsrNode ) ;
723
740
if ( hostDiff !== 0 ) {
724
741
return hostDiff ;
@@ -851,6 +868,25 @@ export function addBlockedChore(
851
868
blockedChores . add ( blockedChore ) ;
852
869
}
853
870
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
+
854
890
function debugChoreTypeToString ( type : ChoreType ) : string {
855
891
return (
856
892
(
0 commit comments