Need help understanding Websockets for Lucee 7

I am having a lot of difficulty trying to get websockets to work correctly. I can talk to my server from a client, but I cannot figure out how to talk back. The docs on the subject are not quite clear enough, and google/AI is getting itself mixed up between the older version of websockets and the new version.

here is my current websocket layout, mostly copied from the example:

component hint="websockets" 
{

    Application.datasourceName = 'mytable';
    
    public static function onFirstOpen( wsclients ) 
    {
        static.wsclients = arguments.wsclients;
        local.dt = {};
        dt.message = 'ehhlol';
        static.wsclients.broadcast(serializeJSON(dt));

        
        thread name="threadDataQueue" oClients=static.wsclients 
        {
            while( attributes.oClients.size() > 0 ) 
            {
                local.dt = {};
                dt.message = 'main branch';
                dt.clientSize = attributes.oClients.size();
                attributes.oClients.broadcast(serializeJSON(dt));
                
                sleep(1000);
            }
        }
        
    }

    function onOpen( wsclient )
    {
        
        //static.wsclients.broadcast("There are now #static.wsclients.size()# connections");
        //onOpen(), we have no way of identifying who just made contact, need to wait for the 1st message with information about user to be sent
        //arguments.wsclient.send( 'incoming' );


    }

    function onOpenAsync( wsclient )
    {

    }

    function onMessage( wsclient, message )
    {
        if( isJSON(arguments.message))
        {   
            local.msg = deserializeJSON(arguments.message);
            if( structKeyExists(msg, 'init') )
            {
                local.wsInfo = websocketInfo(false);
                local.wsInstances = wsInfo.instances;

                for ( var wsI in wsInstances ) 
                {
                    local.thisQuery = wsI.session.queryString;
                    local.params = {};
                    listEach(thisQuery, function(paramPair)
                        {
                            local.key = listFirst(paramPair, "=");
                            local.value = listLast(paramPair, "=");

                            params[key] = value;
                        }, "&");

                    try 
                    {
                        local.queryService = new query(datasource = "#Application.datasourceName#", maxrows="1");
                        local.sql = 'SELECT ID
                                    FROM websocket
                                    WHERE ID = :ID AND token = :token AND userID = :userID';
                        queryService.setSQL(sql);
                        
                        queryService.addParam( name='ID', value='#msg.ID#', cfsqltype='cf_sql_bigint');
                        queryService.addParam( name='userID', value='#msg.userID#', cfsqltype='cf_sql_bigint');
                        queryService.addParam( name='token', value='#params['uuid']#', cfsqltype='cf_sql_longvarchar');
                        
                        local.qFind = queryService.execute().getResult();

                        if( qFind.recordcount > 0 )
                        {
                            local.queryService = new query(datasource = "#Application.datasourceName#");
                            local.sql = 'UPDATE websocket
                                        SET websocketID = :webID
                                        WHERE ID = :ID';
                            queryService.setSQL(sql);
                            
                            queryService.addParam( name='webID', value='#wsI.session.id#', cfsqltype='cf_sql_longvarchar');
                            queryService.addParam( name='ID', value='#qFind.ID#', cfsqltype='cf_sql_longvarchar');
                            
                            local.qUpdate = queryService.execute();
                            break;
                        }
                    }
                    catch(any e)
                    {
                        //failed
                        arguments.wsclient.send( e.Message );
                        arguments.wsclient.close();
                    }
                    
                }
            }
            else
            {
                //receiving a non init message from client, this should contain no data, just used to keepAlive
                //update users activeCon
                if( structKeyExists(msg, "heartbeat") )
                {
                    local.wsInfo = websocketInfo(false);
                    local.wsInstances = wsInfo.instances;
                    local.found = 0;

                    for ( var wsI in wsInstances ) 
                    {
                        if( structKeyExists(wsI.session.requestParameter, 'uuid'))
                        {
                            try 
                            {
                                local.queryService = new query(datasource = "#Application.datasourceName#", maxrows="1");
                                local.sql = 'SELECT ID, userID
                                            FROM websocket
                                            WHERE token = :token AND userID = :userID';
                                queryService.setSQL(sql);
                                
                                queryService.addParam( name='token', value='#wsI.session.requestParameter.uuid[1]#', cfsqltype='cf_sql_longvarchar');
                                queryService.addParam( name='userID', value='#msg.userID#', cfsqltype='cf_sql_bigint');
                                
                                local.qFind = queryService.execute().getResult();

                                if( qFind.recordcount > 0)
                                {   
                                    //found the userID who sent the message
                                    local.queryService = new query(datasource = "#Application.datasourceName#");
                                    local.sql = 'UPDATE users
                                                SET activeCon = :activeCon
                                                WHERE ID = :ID';
                                    queryService.setSQL(sql);
                                    
                                    queryService.addParam( name='activeCon', value='#Now()#', cfsqltype='cf_sql_datetime');
                                    queryService.addParam( name='ID', value='#qFind.userID#', cfsqltype='cf_sql_bigint');                                    
                                    local.qUpdate = queryService.execute();

                                    //update the websocket time too
                                    local.queryService = new query(datasource = "#Application.datasourceName#");
                                    local.sql = 'UPDATE websocket
                                                SET lastModified = :webService
                                                WHERE ID = :ID';
                                    queryService.setSQL(sql);
                                    
                                    queryService.addParam( name='webService', value='#Now()#', cfsqltype='cf_sql_datetime');
                                    queryService.addParam( name='ID', value='#qFind.ID#', cfsqltype='cf_sql_bigint');
                                    
                                    local.qUpdate = queryService.execute();
                                    local.dt = {};
                                    dt.type = 0;
                                    dt.heartbeat = 1;
                                    arguments.wsclient.send( serializeJSON(dt) );
                                    found++;
                                    break;
                                }
                            }
                            catch(any e)
                            {
                                //close connection, there is an error
                                arguments.wsclient.send( e.Message );
                                arguments.wsclient.close();
                            }
                        }
                        else 
                        {
                            //close connection, something is wrong
                            arguments.wsclient.close();
                        }
                    }

                    if( found == 0)
                    {
                       arguments.wsclient.close(); 
                    }
                }
            }
        }
        local.dt = {};
        dt.type = 0;
        return dt;
    }

    function onClose( wsclient, reasonPhrase )
    {
        //delete specfic websocket from db because a new one will be started on refresh/close
        local.wsInfo = websocketInfo(false);
        local.wsInstances = wsInfo.instances;

        for ( var wsI in wsInstances ) 
        {
            //wsI.session has all info
            try 
            {
                local.queryService = new query(datasource = "#Application.datasourceName#", maxrows="1");
                local.sql = 'DELETE FROM websocket
                            WHERE websocketID = :webID AND userID = :userID';
                queryService.setSQL(sql);
                
                queryService.addParam( name='webID', value='#wsI.session.id#', cfsqltype='cf_sql_longvarchar');
                queryService.addParam( name='userID', value='#msg.userID#', cfsqltype='cf_sql_bigint');
                
                local.qClose = queryService.execute().getResult();

            }
            catch(any e)
            {
                //failed
                arguments.wsclient.send( e.Message );
            }
            
        }
    }

    function onError( wsclient, cfcatch )
    {

    }

    public static function onLastClose()
    {

    }

    public void function sendMessage( required string jsonData) 
    {
        variables.wsclient.send(jsonData);
    }
}

And I run a little test script to send messages:

<cfscript>
wsInfo = websocketInfo(false);

//if ( !wsInfo.instances.len() )
//    return;

wsInstances = wsInfo.instances;

//var item = getRedisData();
//var stItem = deserializeJSON( item );
for ( wsI in wsInstances ) 
{
	queryService = new query(datasource = GetApplicationSettings().defaultdatasource);
	sql = 'SELECT userID
				FROM webebsocket
				WHERE websocketID = :socket';
	queryService.setSQL(sql);

	queryService.addParam( name='socket', value='#wsI.session.id#', cfsqltype='cf_sql_varchar');
		
	qWeb = queryService.execute().getResult();

	if( qWeb.recordCount > 0 )
	{
		queryService = new query(datasource = GetApplicationSettings().defaultdatasource);
		sql = 'SELECT *
					FROM msg
					WHERE userID = :userID';
		queryService.setSQL(sql);
		
		queryService.addParam( name='userID', value='#qWeb.userID#', cfsqltype='cf_sql_bigint');
		
		qMsgs = queryService.execute().getResult();
writeDump(qMsgs);
		cfloop( query="qMsgs")
		{
writeDump(ID)
			dt = {}
			dt.type = 0;
			dt.message = '';

			if( !isNull(joinID) )
			{
				//join message
				dt.type = 1;
				dt.message = 'You have been invited into a room!'
			}
			wsI.component.sendMessage(serializeJSON(dt))
		}
	}

	//writeDump(wsI);
	writeDump(wsI.session);
	writeDump(wsI.component);
    //if ( GetMetadata( wsI.component ).name == 'test' && wsI.component.hasRole( stItem.data.role ) ) {
    //    wsI.component.sendMessage( item );
    //}
}
</cfscript>

And of course, after all of that, I get an error:

Lucee 7.0.2.106 Error (expression)
Message 	Component [/websocket.cfc] has no accessible Member with name [wsclient]
AI 	For AI-driven exception analysis setup, see AI Setup Guide.
Function  sendMessage
Component  websocket

My biggest issue is, I see that I have a static.wsclients, but I do not understand how I parse thru it to find the .send() method for a specific client.

Another issue I am having is onFirstOpen( wsclients ) only runs the thread for 50 seconds vs forever. And the 50 seconds I am assuming is the timeout for the websocket. So any guidance on that might help as well.

1 Like

I have completely reworked the doc and added a a shit load more tests

Let me know if that answers your questions

The reflection stuff is broken and needs a comprehensive rework which I am working on, it’s bloody complicated, stay tuned for updates on that one

1 Like

Thank you, this was some much needed clarification.

I was just starting to figure out the fact that I was going to have to store the wsClient from the onOpen myself (had thought it was part of the socket given the code example) and reference it when needed.

I also had come to the conclusion that putting the thread in onFirstOpen() was not going to work given the timeout and I was going to create an Event Gateway (Until I read the docs) or some kind of scheduled event/cron.d job to run.

But this is my last hurdle to this webapp I am rebuilding, and hope for a launch very soon!! And if it catches on like I hope it will, I can finally become a donating member of Lucee instead of a back end lurker. :partying_face:

1 Like

Might I request a few amendments to your new example?

One thing to note is that every page refresh or new tab open on the page will generate a new websocket connection. In your example you are returning a clientId. Might I suggest you return the websocketId so one is not overriding each new connection with the new wsClient? That way one can maintain a websocket across all the pages that are opened.

        local.uuid = cgi.query_string.listLast( "=" );
        local.socketID = 0;

        local.wsInfo = websocketInfo(false);
        local.wsInstances = wsInfo.instances;

        for ( var wsI in wsInstances ) 
        {
            local.thisQuery = wsI.session.queryString;
            local.params = {};
            listEach(thisQuery, function(paramPair)
            {
                local.key = listFirst(paramPair, "=");
                local.value = listLast(paramPair, "=");

                params[key] = value;
            }, "&");

            if( params['uuid'] == uuid)
            {
                return wsI.session.id;
            }           
        }

Edit: The most important point of this is that browsers do not automatically close websockets on a page refresh.

One last note/question.

Are you intending to use the client scope for the websocket? In your recipe you use var client = static.clientsByUser[ arguments.userId ];

And then later client.send( serializeJSON(dt) );

Which throws an error that the client scope is not available (well for me, since I am not using client scope).