11import { isObject , isRecord } from "@commontools/utils/types" ;
22import {
33 type Cancel ,
4- type Cell ,
54 convertCellsToLinks ,
65 effect ,
7- isCell ,
86 type JSONSchema ,
97 UI ,
108 useCancelGroup ,
119} from "@commontools/runner" ;
10+ import { isRemoteCell , type RemoteCell } from "@commontools/runner/worker" ;
1211import { isVNode , type Props , type RenderNode , type VNode } from "./jsx.ts" ;
1312import * as logger from "./logger.ts" ;
1413
@@ -22,7 +21,7 @@ export interface RenderOptions {
2221 setProp ?: SetPropHandler ;
2322 document ?: Document ;
2423 /** The root cell for auto-wrapping with ct-cell-context on [UI] traversal */
25- rootCell ?: Cell ;
24+ rootCell ?: RemoteCell < VNode > ;
2625}
2726
2827export const vdomSchema : JSONSchema = {
@@ -60,25 +59,30 @@ export const vdomSchema: JSONSchema = {
6059 */
6160export const render = (
6261 parent : HTMLElement ,
63- view : VNode | Cell < VNode > ,
62+ view : VNode | RemoteCell < VNode > ,
6463 options : RenderOptions = { } ,
6564) : Cancel => {
66- // Initialize visited set with the original cell for cycle detection
67- const visited = new Set < object > ( ) ;
68- let rootCell : Cell | undefined ;
69-
70- if ( isCell ( view ) ) {
71- visited . add ( view ) ;
72- rootCell = view ; // Capture the original cell for ct-cell-context wrapping
73- view = view . asSchema ( vdomSchema ) ;
65+ let rootCell : RemoteCell < VNode > | undefined ;
66+
67+ if ( isRemoteCell ( view ) ) {
68+ rootCell = view as RemoteCell < VNode > ; // Capture the original cell for ct-cell-context wrapping
69+ view = rootCell . asSchema ( vdomSchema ) as RemoteCell < VNode > ;
7470 }
7571
7672 // Pass rootCell through options if we have one
7773 const optionsWithCell = rootCell ? { ...options , rootCell } : options ;
7874
7975 return effect (
80- view ,
81- ( view : VNode ) => renderImpl ( parent , view , optionsWithCell , visited ) ,
76+ view as VNode ,
77+ ( view : VNode ) => {
78+ // Create a fresh visited set for each render pass.
79+ // This prevents false cycle detection when re-rendering with updated values.
80+ const visited = new Set < object > ( ) ;
81+ if ( rootCell ) {
82+ visited . add ( rootCell ) ;
83+ }
84+ return renderImpl ( parent , view , optionsWithCell , visited ) ;
85+ } ,
8286 ) ;
8387} ;
8488
@@ -131,7 +135,7 @@ const createCyclePlaceholder = (document: Document): HTMLSpanElement => {
131135/** Check if a cell has been visited, using .equals() for cell comparison */
132136const hasVisitedCell = (
133137 visited : Set < object > ,
134- cell : Cell < unknown > ,
138+ cell : { equals ( other : unknown ) : boolean } ,
135139) : boolean => {
136140 for ( const item of visited ) {
137141 if ( cell . equals ( item ) ) {
@@ -207,7 +211,7 @@ const renderNode = (
207211 if ( cellForContext && element ) {
208212 const wrapper = document . createElement (
209213 "ct-cell-context" ,
210- ) as HTMLElement & { cell ?: Cell } ;
214+ ) as HTMLElement & { cell ?: RemoteCell < VNode > } ;
211215 wrapper . cell = cellForContext ;
212216 wrapper . appendChild ( element ) ;
213217 return [ wrapper , cancel ] ;
@@ -235,13 +239,16 @@ const bindChildren = (
235239 const document = options . document ?? globalThis . document ;
236240
237241 // Check for cell cycle before setting up effect (using .equals() for comparison)
238- if ( isCell ( child ) && hasVisitedCell ( visited , child ) ) {
242+ if (
243+ isRenderableCell ( child ) &&
244+ hasVisitedCell ( visited , child as unknown as RemoteCell < unknown > )
245+ ) {
239246 logger . warn ( "render" , "Cycle detected in cell graph" , child ) ;
240247 return { node : createCyclePlaceholder ( document ) , cancel : ( ) => { } } ;
241248 }
242249
243250 // Track if this child is a cell for the visited set
244- const childIsCell = isCell ( child ) ;
251+ const childIsCell = isRenderableCell ( child ) ;
245252
246253 let currentNode : ChildNode | null = null ;
247254 const cancel = effect ( child , ( childValue ) => {
@@ -377,7 +384,7 @@ const bindProps = (
377384 const setProperty = options . setProp ?? setProp ;
378385 const [ cancel , addCancel ] = useCancelGroup ( ) ;
379386 for ( const [ propKey , propValue ] of Object . entries ( props ) ) {
380- if ( isCell ( propValue ) ) {
387+ if ( isRenderableCell ( propValue ) ) {
381388 // If prop is an event, we need to add an event listener
382389 if ( isEventProp ( propKey ) ) {
383390 const key = cleanEventProp ( propKey ) ;
@@ -538,10 +545,10 @@ const sanitizeScripts = (node: VNode): VNode | null => {
538545 if ( node . name === "script" ) {
539546 return null ;
540547 }
541- if ( ! isCell ( node . props ) && ! isObject ( node . props ) ) {
548+ if ( ! isRenderableCell ( node . props ) && ! isObject ( node . props ) ) {
542549 node = { ...node , props : { } } ;
543550 }
544- if ( ! isCell ( node . children ) && ! Array . isArray ( node . children ) ) {
551+ if ( ! isRenderableCell ( node . children ) && ! Array . isArray ( node . children ) ) {
545552 node = { ...node , children : [ ] } ;
546553 }
547554
@@ -654,3 +661,12 @@ function isSelectElement(value: unknown): value is HTMLSelectElement {
654661 typeof value . tagName === "string" &&
655662 value . tagName . toUpperCase ( ) === "SELECT" ) ;
656663}
664+
665+ type RenderableCell = {
666+ send ( value : unknown ) : void ;
667+ } ;
668+ function isRenderableCell ( value : unknown ) : value is RenderableCell {
669+ // Check for any object with a send() method (Cell, RemoteCell, or Stream)
670+ return ! ! value && typeof value === "object" && "send" in value &&
671+ typeof ( value as RenderableCell ) . send === "function" ;
672+ }
0 commit comments