From 005e946f43dbe92b3d14ecbbbf9c33600efbc7e5 Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Sun, 28 Jun 2026 07:59:31 +0300 Subject: [PATCH 01/23] add WebSocket endpoint support * find all classes annotated with wsEndpoint modifier * validate and extract the four handler functions (onConnect, onReady, onData, onClose) * extract the URI from the modifier params to pass to setWebSocketHandler --- WebPlatform/server.alusus | 71 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/WebPlatform/server.alusus b/WebPlatform/server.alusus index 016762d..166d94d 100644 --- a/WebPlatform/server.alusus +++ b/WebPlatform/server.alusus @@ -276,6 +276,66 @@ } } + func generateWebSockets(parent: ref[Core.Basic.TiObject]) { + + // here we get all the classes with Ws modifier + + def classelEments: Array[ref[Core.Basic.TiObject]] = Spp.astMgr.findElements( + ast { modifier == "wsEndpoint" || modifier == "منفذ_مقبس" }, + parent, + Spp.SeekerFlags.SKIP_OWNERS | Spp.SeekerFlags.SKIP_USES | Spp.SeekerFlags.SKIP_CHILDREN + ); + + // here we will go inside each class to extract the callbacks + + def i: Int; + for i = 0, i < classelEments.getLength(), ++i { + + // here we extract the functions inside class + def handlerElements : Array[ref[Core.Basic.TiObject]] = Spp.astMgr.findElements ( + ast { elementType == "function" }, + classelEments(i), + Spp.SeekerFlags.SKIP_OWNERS | Spp.SeekerFlags.SKIP_USES | Spp.SeekerFlags.SKIP_CHILDREN + ); + + def onConnectElement: ref[Core.Basic.TiObject]; + def onReadyElement: ref[Core.Basic.TiObject]; + def onReceiveElement: ref[Core.Basic.TiObject]; + def onCloseElement: ref[Core.Basic.TiObject]; + + def j: Int; + for j = 0, j < handlerElements.getLength(), ++j { + def name: String = Spp.astMgr.getDefinitionName(handlerElements(j)); + Console.print("this is the name : %s\n" , name.buf ); + if name == "onConnect" || name == "عندالأتصال" { + onConnectElement~no_deref = handlerElements(j); + } else if name == "onReady" || name == "عندالأستعداد" { + onReadyElement~no_deref = handlerElements(j); + } else if name == "onReceive" || name == "عند_الأستلام" { + onReceiveElement~no_deref = handlerElements(j); + } else if name == "onClose" || name == "عند_الاغلاق" { + onCloseElement~no_deref = handlerElements(j); + } else { + System.fail(1, "Invalid WS handler function name"); + } + } + if onConnectElement~ptr == 0 || onReadyElement~ptr == 0 + || onReceiveElement~ptr == 0 || onCloseElement~ptr == 0 { + System.fail(1, "Missing one or more WS handler functions"); + } + + def modifier: ref[Core.Basic.TiObject](Spp.astMgr.findModifierForElement(classelEments(i), "wsEndpoint")); + if modifier~ptr == 0 modifier~no_deref = Spp.astMgr.findModifierForElement(classelEments(i), "منفذ_مقبس"); + + // get Uri + def wsEndpointUris: Array[String]; + if !Spp.astMgr.getModifierStringParams(modifier, wsEndpointUris) + || wsEndpointUris.getLength() != 1 { + System.fail(1, "Invalid WS endpoint param"); + } + } + + } // Querying Functions func getAssetsRoutesFromModules (modulesRef: ref[Core.Basic.TiObject]): Array[StaticRoute] { @@ -347,6 +407,17 @@ ); if httpContext == 0 return 0; + preprocess { + def modules: Array[ref[Core.Basic.TiObject]] = getAllModules(modulesRef~ast); + if modules.getLength() == 0 { + Spp.buildMgr.raiseBuildNotice("SPPA1002", 1, modulesRef~ast); + } + def i: Int; + for i = 0, i < modules.getLength(), ++i generateWebSockets(modules(i)); + }; + + + def session: ptr[ServerSession] = Memory.alloc(ServerSession~size)~cast[ptr[ServerSession]]; session~cnt~init(); session~cnt.requestCallbackContext = requestCallbackContext; From 6fa037784124322d64ae43be1359ca87e18dd967 Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Sun, 28 Jun 2026 18:44:33 +0300 Subject: [PATCH 02/23] implement WebSocket handler construction * add wsConnection class with isDead flag and guarded send/close methods * implement constructSocketConnectHandler to call user onConnect with raw connection ptr * implement constructSocketReadyHandler to look up wsConnection and call user onReady * implement constructSocketDataHandler with isBinary flag derived from opcode * implement constructSocketCloseHandler to nullify connection, call user onClose, then remove from map * add socketConnections map to track active connections by ptr[Http.Connection] --- WebPlatform/server.alusus | 147 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 143 insertions(+), 4 deletions(-) diff --git a/WebPlatform/server.alusus b/WebPlatform/server.alusus index 166d94d..f223732 100644 --- a/WebPlatform/server.alusus +++ b/WebPlatform/server.alusus @@ -47,6 +47,43 @@ class ServerSession { def requestCallbackContext: SrdRef[RequestCallbackContext]; def httpContext: ptr[Http.Context]; + + def socketConnections: Map[ptr[Http.Connection], SrdRef[wsConnection]]; + } + + class wsConnection { + + def _connection : ptr[Http.Connection]; + def isDead : Bool; + + handler this~init(connection: ptr[Http.Connection]) { + this._connection = connection; + this.isDead = 0; + } + handler this.dead () { + this._connection = 0; + this.isDead = 1; + } + + handler this.sendText (data: CharsPtr, dataLen: ArchWord) : Int { + if this.isDead return 0; + return Http.writeTextToWebSocket(this._connection, data, dataLen); + } + + handler this.sendText (data: CharsPtr) : Int { + if this.isDead return 0; + return Http.writeTextToWebSocket(this._connection, data); + } + + handler this.sendBinary (data: CharsPtr, dataLen: ArchWord) : Int { + if this.isDead return 0; + return Http.writeBinaryToWebSocket(this._connection , data , dataLen); + } + + handler this.close () : Int { + if this.isDead return 0; + return Http.closeWebSocket(this._connection); + } } def AstTemplateMap: alias Map[String, ref[Core.Basic.TiObject]]; @@ -327,12 +364,30 @@ def modifier: ref[Core.Basic.TiObject](Spp.astMgr.findModifierForElement(classelEments(i), "wsEndpoint")); if modifier~ptr == 0 modifier~no_deref = Spp.astMgr.findModifierForElement(classelEments(i), "منفذ_مقبس"); - // get Uri - def wsEndpointUris: Array[String]; - if !Spp.astMgr.getModifierStringParams(modifier, wsEndpointUris) - || wsEndpointUris.getLength() != 1 { + def wsEndpointParam: Array[String]; + if !Spp.astMgr.getModifierStringParams(modifier, wsEndpointParam) + || wsEndpointParam.getLength() != 1 { System.fail(1, "Invalid WS endpoint param"); } + + Spp.astMgr.insertAst( + ast { + Http.setWebSocketHandler ( + httpContext, + {{wsEndpointUri}}, + {{connectHandler}}, + {{readyHandler}}, + {{dataHandler}}, + {{closeHandler}}, + ) + }, + AstTemplateMap() + .set(Srl.String("wsEndpointUri"), Core.Basic.TiStr(wsEndpointParam(0))) + .set(Srl.String("connectHandler"), constructSocketConnectHandler(onConnectElement)) + .set(Srl.String("readyHandler"), constructSocketReadyHandler(onReadyElement)) + .set(Srl.String("dataHandler"), constructSocketDataHandler(onReceiveElement)) + .set(Srl.String("closeHandler"), constructSocketCloseHandler(onCloseElement)) + ); } } @@ -407,6 +462,8 @@ ); if httpContext == 0 return 0; + def socketConnections: Map[ptr[Http.Connection], SrdRef[wsConnection]]; + preprocess { def modules: Array[ref[Core.Basic.TiObject]] = getAllModules(modulesRef~ast); if modules.getLength() == 0 { @@ -647,4 +704,86 @@ System.fail(1, String("Invalid asset route; path should end with /: ") + buildPath); } } + + func constructSocketConnectHandler (element: ref[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { + return Spp.astMgr.buildAst( + ast { + func (connection: ptr[Http.Connection], userData: ptr[Void]) : Int { + def output : Int = providedHandlerFullRef(connection); + return output; + } + }, + AstTemplateMap() + .set(Srl.String("providedHandlerFullRef"), constructElementFullReference(element)) + ) + } + + func constructSocketReadyHandler (element: ref[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { + return Spp.astMgr.buildAst( + ast { + func (connection: ptr[Connection], userData: ptr[Void]) : Void { + def ws: SrdRef[wsConnection]; + ws.alloc(); + ws.obj~init(connection); + socketConnections.set(connection, ws); + + providedHandlerFullRef(ws); + } + }, + AstTemplateMap() + .set(Srl.String("providedHandlerFullRef"), constructElementFullReference(element)) + ) + } + + func constructSocketDataHandler(element: ref[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { + return Spp.astMgr.buildAst( + ast { + func (connection: ptr[Http.Connection], opcode: Int, data: CharsPtr, dataLen: ArchWord, userData: ptr[Void]): Int { + def pos: ArchInt = socketConnections.findPos(connection); + + if (pos == -1) { + System.fail(1, "Data sent to already closed connection!"); + } + + def ws: SrdRef[wsConnection] = socketConnections.valAt(pos); + + // opcode will be either 1 or 2 + // 1 for text data + // 2 for binary data + + def isBinary: Bool = opcode == 2; + providedHandlerFullRef(ws, data, dataLen, isBinary); + + // keep connection alive + // if the developer need to close the connection he can call the wsConnection.close() + + return 1; + } + }, + AstTemplateMap() + .set(Srl.String("providedHandlerFullRef"), constructElementFullReference(element)) + ); + } + + func constructSocketCloseHandler(element: ref[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { + return Spp.astMgr.buildAst( + ast { + func (connection: ptr[Http.Connection], userData: ptr[Void]): Void { + def pos: ArchInt = socketConnections.findPos(connection); + if pos == -1 { + System.fail(1, "Tried to close an already closed connection!"); + } + def ws: SrdRef[wsConnection] = socketConnections.valAt(pos); + ws.dead(); + + providedHandlerFullRef(ws); + + socketConnections.removeAt(pos); + } + }, + AstTemplateMap() + .set(Srl.String("providedHandlerFullRef"), constructElementFullReference(element)) + ); + } + } From 1d9be511692e42b39137c7ee94967525249054f4 Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Tue, 30 Jun 2026 08:54:49 +0300 Subject: [PATCH 03/23] Fix WebSocket handler generation in server.alusus * Reference handlers by node pointer (Passage), not by name path * Pass the function node to setWebSocketHandler, not its wrapping block * Keep handler blocks alive across insertAst (avoid use-after-free) * Use ArchInt (cast from connection ptr) as the socketConnections key --- WebPlatform/server.alusus | 104 +++++++++++++++++++------------------- 1 file changed, 53 insertions(+), 51 deletions(-) diff --git a/WebPlatform/server.alusus b/WebPlatform/server.alusus index f223732..8c8beea 100644 --- a/WebPlatform/server.alusus +++ b/WebPlatform/server.alusus @@ -47,8 +47,7 @@ class ServerSession { def requestCallbackContext: SrdRef[RequestCallbackContext]; def httpContext: ptr[Http.Context]; - - def socketConnections: Map[ptr[Http.Connection], SrdRef[wsConnection]]; + def socketConnections: Map[ArchInt, SrdRef[wsConnection]]; } class wsConnection { @@ -343,7 +342,6 @@ def j: Int; for j = 0, j < handlerElements.getLength(), ++j { def name: String = Spp.astMgr.getDefinitionName(handlerElements(j)); - Console.print("this is the name : %s\n" , name.buf ); if name == "onConnect" || name == "عندالأتصال" { onConnectElement~no_deref = handlerElements(j); } else if name == "onReady" || name == "عندالأستعداد" { @@ -372,21 +370,21 @@ Spp.astMgr.insertAst( ast { - Http.setWebSocketHandler ( - httpContext, - {{wsEndpointUri}}, - {{connectHandler}}, - {{readyHandler}}, - {{dataHandler}}, - {{closeHandler}}, - ) - }, + Http.setWebSocketHandler( + httpContext, + "{{wsEndpointUri}}", + connectHandler, + readyHandler, + dataHandler, + closeHandler, + session + )}, AstTemplateMap() - .set(Srl.String("wsEndpointUri"), Core.Basic.TiStr(wsEndpointParam(0))) - .set(Srl.String("connectHandler"), constructSocketConnectHandler(onConnectElement)) - .set(Srl.String("readyHandler"), constructSocketReadyHandler(onReadyElement)) - .set(Srl.String("dataHandler"), constructSocketDataHandler(onReceiveElement)) - .set(Srl.String("closeHandler"), constructSocketCloseHandler(onCloseElement)) + .set(Srl.String("wsEndpointUri"), Core.Basic.TiStr(wsEndpointParam(0))) + .set(Srl.String("connectHandler"), firstChildOf(constructSocketConnectHandler(onConnectElement))) + .set(Srl.String("readyHandler"), firstChildOf(constructSocketReadyHandler(onReadyElement))) + .set(Srl.String("dataHandler"), firstChildOf(constructSocketDataHandler(onReceiveElement))) + .set(Srl.String("closeHandler"), firstChildOf(constructSocketCloseHandler(onCloseElement))) ); } @@ -462,7 +460,10 @@ ); if httpContext == 0 return 0; - def socketConnections: Map[ptr[Http.Connection], SrdRef[wsConnection]]; + def session: ptr[ServerSession] = Memory.alloc(ServerSession~size)~cast[ptr[ServerSession]]; + session~cnt~init(); + session~cnt.requestCallbackContext = requestCallbackContext; + session~cnt.httpContext = httpContext; preprocess { def modules: Array[ref[Core.Basic.TiObject]] = getAllModules(modulesRef~ast); @@ -473,12 +474,6 @@ for i = 0, i < modules.getLength(), ++i generateWebSockets(modules(i)); }; - - - def session: ptr[ServerSession] = Memory.alloc(ServerSession~size)~cast[ptr[ServerSession]]; - session~cnt~init(); - session~cnt.requestCallbackContext = requestCallbackContext; - session~cnt.httpContext = httpContext; return session; } @@ -544,12 +539,16 @@ def i: Int; for i = 0, i < modules.getLength(), ++i generateBeEndpointsCalls(modules(i)); }; - + if String.isEqual(method, "GET") { if String.isEqual(uri, "/api.js") { Http.sendFile(connection, context.mainAssetsPath + "api.js"); return 1; } + + if String.isEqual(uri, "/chat") { + return 0; + } if !String.isEqual(uri, "/") { def fileName: String = context.uiEndpointsPath + uri; @@ -602,23 +601,22 @@ // Helpers + // refer to element to an AST element using its pointer instead of referring to that element using an identifier + // makes generating generating code dynamically easier func constructElementFullReference (element: ref[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { - def node: ref[Core.Data.Node](castRef[element, Core.Data.Node]); - if node.owner~ptr == 0 return SrdRef[Core.Basic.TiObject](); - def name: String = Spp.astMgr.getDefinitionName(node); - def identifier: SrdRef[Core.Data.Ast.Identifier] = Core.Basic.newSrdObj[Core.Data.Ast.Identifier].{ - value.value = name; - }; - def ownerRef: SrdRef[Core.Basic.TiObject] = constructElementFullReference(node.owner.owner); - if ownerRef.isNull() return identifier - else return Core.Basic.newSrdObj[Core.Data.Ast.LinkOperator].{ - Core.Basic.MapContainerOf[this].{ - setElement("first", ownerRef); - setElement("second", identifier); - }; + return Core.Basic.newSrdObj[Core.Data.Ast.Passage].{ + Core.Basic.BindingOf[this].setMember("target", element); }; } + // return reference for the first node child inside Block node + func firstChildOf (result: ref[SrdRef[Core.Basic.TiObject]]): ref[Core.Basic.TiObject] { + def block: SrdRef[Spp.Ast.Block] = Core.Basic.dynCastSrdRef[result, Spp.Ast.Block]; + def container: ref[Core.Basic.Containing]; + container~no_deref = Core.Basic.ContainerOf[block]; + return container.getElement(0); + } + func getModifierParams (element: ref[Core.Basic.TiObject], enKwd: CharsPtr, arKwd: CharsPtr): Array[String] { def modifier: ref[Core.Basic.TiObject](Spp.astMgr.findModifierForElement(element, enKwd)); if modifier~ptr == 0 modifier~no_deref = Spp.astMgr.findModifierForElement(element, arKwd); @@ -705,8 +703,9 @@ } } - func constructSocketConnectHandler (element: ref[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { - return Spp.astMgr.buildAst( + // return block element with the wrapper function element as its first child + func constructSocketConnectHandler (element: ref[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { + return Spp.astMgr.buildAst( ast { func (connection: ptr[Http.Connection], userData: ptr[Void]) : Int { def output : Int = providedHandlerFullRef(connection); @@ -715,17 +714,18 @@ }, AstTemplateMap() .set(Srl.String("providedHandlerFullRef"), constructElementFullReference(element)) - ) + ); } - + // return block element with the wrapper function element as its first child func constructSocketReadyHandler (element: ref[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { return Spp.astMgr.buildAst( ast { - func (connection: ptr[Connection], userData: ptr[Void]) : Void { + func (connection: ptr[Http.Connection], userData: ptr[Void]) : Void { + def connectionsMap: Map[ArchInt, SrdRef[wsConnection]] = userData~cast[ptr[ServerSession]]~cnt.socketConnections; def ws: SrdRef[wsConnection]; ws.alloc(); ws.obj~init(connection); - socketConnections.set(connection, ws); + connectionsMap.set(connection~cast[ArchInt], ws); providedHandlerFullRef(ws); } @@ -734,18 +734,19 @@ .set(Srl.String("providedHandlerFullRef"), constructElementFullReference(element)) ) } - + // return block element with the wrapper function element as its first child func constructSocketDataHandler(element: ref[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { return Spp.astMgr.buildAst( ast { func (connection: ptr[Http.Connection], opcode: Int, data: CharsPtr, dataLen: ArchWord, userData: ptr[Void]): Int { - def pos: ArchInt = socketConnections.findPos(connection); + def connectionsMap: Map[ArchInt, SrdRef[wsConnection]] = userData~cast[ptr[ServerSession]]~cnt.socketConnections; + def pos: ArchInt = connectionsMap.findPos(connection~cast[ArchInt]); if (pos == -1) { System.fail(1, "Data sent to already closed connection!"); } - def ws: SrdRef[wsConnection] = socketConnections.valAt(pos); + def ws: SrdRef[wsConnection] = connectionsMap.valAt(pos); // opcode will be either 1 or 2 // 1 for text data @@ -764,21 +765,22 @@ .set(Srl.String("providedHandlerFullRef"), constructElementFullReference(element)) ); } - + // return block element with the wrapper function element as its first child func constructSocketCloseHandler(element: ref[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { return Spp.astMgr.buildAst( ast { func (connection: ptr[Http.Connection], userData: ptr[Void]): Void { - def pos: ArchInt = socketConnections.findPos(connection); + def connectionsMap: Map[ArchInt, SrdRef[wsConnection]] = userData~cast[ptr[ServerSession]]~cnt.socketConnections; + def pos: ArchInt = connectionsMap.findPos(connection~cast[ArchInt]); if pos == -1 { System.fail(1, "Tried to close an already closed connection!"); } - def ws: SrdRef[wsConnection] = socketConnections.valAt(pos); + def ws: SrdRef[wsConnection] = connectionsMap.valAt(pos); ws.dead(); providedHandlerFullRef(ws); - socketConnections.removeAt(pos); + connectionsMap.removeAt(pos); } }, AstTemplateMap() From 37b4ea04643fe7a7670aa8e37c73bca31a85c6ef Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Wed, 1 Jul 2026 00:27:09 +0300 Subject: [PATCH 04/23] Generate URI checks for @wsEndpoint paths in requestCallback --- WebPlatform/server.alusus | 89 ++++++++++++------------ WebPlatform/web_socket_connection.alusus | 34 +++++++++ 2 files changed, 78 insertions(+), 45 deletions(-) create mode 100644 WebPlatform/web_socket_connection.alusus diff --git a/WebPlatform/server.alusus b/WebPlatform/server.alusus index 8c8beea..2fd5144 100644 --- a/WebPlatform/server.alusus +++ b/WebPlatform/server.alusus @@ -50,41 +50,6 @@ def socketConnections: Map[ArchInt, SrdRef[wsConnection]]; } - class wsConnection { - - def _connection : ptr[Http.Connection]; - def isDead : Bool; - - handler this~init(connection: ptr[Http.Connection]) { - this._connection = connection; - this.isDead = 0; - } - handler this.dead () { - this._connection = 0; - this.isDead = 1; - } - - handler this.sendText (data: CharsPtr, dataLen: ArchWord) : Int { - if this.isDead return 0; - return Http.writeTextToWebSocket(this._connection, data, dataLen); - } - - handler this.sendText (data: CharsPtr) : Int { - if this.isDead return 0; - return Http.writeTextToWebSocket(this._connection, data); - } - - handler this.sendBinary (data: CharsPtr, dataLen: ArchWord) : Int { - if this.isDead return 0; - return Http.writeBinaryToWebSocket(this._connection , data , dataLen); - } - - handler this.close () : Int { - if this.isDead return 0; - return Http.closeWebSocket(this._connection); - } - } - def AstTemplateMap: alias Map[String, ref[Core.Basic.TiObject]]; // Build Functions @@ -268,7 +233,7 @@ func generateBeEndpointsCalls (parent: ref[Core.Basic.TiObject]) { def elements: Array[ref[Core.Basic.TiObject]] = Spp.astMgr.findElements( - ast { modifier == "beEndpoint" || modifier == "منفذ_بياني" }, + ast { modifier == "beEndpoint" || modifier == "منفذ_بياني"}, parent, Spp.SeekerFlags.SKIP_OWNERS | Spp.SeekerFlags.SKIP_USES | Spp.SeekerFlags.SKIP_CHILDREN ); @@ -311,6 +276,35 @@ ); } } + func generateWebSocketEndpointChecks (parent: ref[Core.Basic.TiObject]) { + def classElements: Array[ref[Core.Basic.TiObject]] = Spp.astMgr.findElements( + ast { modifier == "wsEndpoint" || modifier == "منفذ_مقبس" }, + parent, + Spp.SeekerFlags.SKIP_OWNERS | Spp.SeekerFlags.SKIP_USES | Spp.SeekerFlags.SKIP_CHILDREN + ); + def i: Int; + for i = 0, i < classElements.getLength(), ++i { + def modifier: ref[Core.Basic.TiObject](Spp.astMgr.findModifierForElement(classElements(i), "wsEndpoint")); + if modifier~ptr == 0 modifier~no_deref = Spp.astMgr.findModifierForElement(classElements(i), "منفذ_مقبس"); + + def wsEndpointParam: Array[String]; + + if !Spp.astMgr.getModifierStringParams(modifier, wsEndpointParam) + || wsEndpointParam.getLength() != 1 { + System.fail(1, "Invalid WS endpoint params"); + } + + Spp.astMgr.insertAst( + ast { + if String.isEqual(method, "GET") && String.isEqual(uri, "{{wsEndpointUri}}") { + return 0; + } + }, + AstTemplateMap() + .set(Srl.String("wsEndpointUri"), Core.Basic.TiStr(wsEndpointParam(0))) + ); + } + } func generateWebSockets(parent: ref[Core.Basic.TiObject]) { @@ -537,18 +531,17 @@ Spp.buildMgr.raiseBuildNotice("SPPA1002", 1, modulesRef~ast); } def i: Int; - for i = 0, i < modules.getLength(), ++i generateBeEndpointsCalls(modules(i)); + for i = 0, i < modules.getLength(), ++i { + generateBeEndpointsCalls(modules(i)); + generateWebSocketEndpointChecks(modules(i)); + }; }; - + if String.isEqual(method, "GET") { if String.isEqual(uri, "/api.js") { Http.sendFile(connection, context.mainAssetsPath + "api.js"); return 1; } - - if String.isEqual(uri, "/chat") { - return 0; - } if !String.isEqual(uri, "/") { def fileName: String = context.uiEndpointsPath + uri; @@ -721,7 +714,9 @@ return Spp.astMgr.buildAst( ast { func (connection: ptr[Http.Connection], userData: ptr[Void]) : Void { - def connectionsMap: Map[ArchInt, SrdRef[wsConnection]] = userData~cast[ptr[ServerSession]]~cnt.socketConnections; + def connectionsMap: ref[Map[ArchInt, SrdRef[wsConnection]]]; + connectionsMap~no_deref = userData~cast[ptr[ServerSession]]~cnt.socketConnections; + def ws: SrdRef[wsConnection]; ws.alloc(); ws.obj~init(connection); @@ -739,7 +734,9 @@ return Spp.astMgr.buildAst( ast { func (connection: ptr[Http.Connection], opcode: Int, data: CharsPtr, dataLen: ArchWord, userData: ptr[Void]): Int { - def connectionsMap: Map[ArchInt, SrdRef[wsConnection]] = userData~cast[ptr[ServerSession]]~cnt.socketConnections; + def connectionsMap: ref[Map[ArchInt, SrdRef[wsConnection]]]; + connectionsMap~no_deref = userData~cast[ptr[ServerSession]]~cnt.socketConnections; + def pos: ArchInt = connectionsMap.findPos(connection~cast[ArchInt]); if (pos == -1) { @@ -770,7 +767,9 @@ return Spp.astMgr.buildAst( ast { func (connection: ptr[Http.Connection], userData: ptr[Void]): Void { - def connectionsMap: Map[ArchInt, SrdRef[wsConnection]] = userData~cast[ptr[ServerSession]]~cnt.socketConnections; + def connectionsMap: ref[Map[ArchInt, SrdRef[wsConnection]]]; + connectionsMap~no_deref = userData~cast[ptr[ServerSession]]~cnt.socketConnections; + def pos: ArchInt = connectionsMap.findPos(connection~cast[ArchInt]); if pos == -1 { System.fail(1, "Tried to close an already closed connection!"); diff --git a/WebPlatform/web_socket_connection.alusus b/WebPlatform/web_socket_connection.alusus new file mode 100644 index 0000000..955e9d5 --- /dev/null +++ b/WebPlatform/web_socket_connection.alusus @@ -0,0 +1,34 @@ +class wsConnection { + def _connection : ptr[Http.Connection]; + def isDead : Bool; + + handler this~init(connection: ptr[Http.Connection]) { + this._connection = connection; + this.isDead = 0; + } + + handler this.dead () { + this._connection = 0; + this.isDead = 1; + } + + handler this.sendText (data: CharsPtr, dataLen: ArchWord) : Int { + if this.isDead return 0; + return Http.writeTextToWebSocket(this._connection, data, dataLen); + } + + handler this.sendText (data: CharsPtr) : Int { + if this.isDead return 0; + return Http.writeTextToWebSocket(this._connection, data); + } + + handler this.sendBinary (data: CharsPtr, dataLen: ArchWord) : Int { + if this.isDead return 0; + return Http.writeBinaryToWebSocket(this._connection , data , dataLen); + } + + handler this.close () : Int { + if this.isDead return 0; + return Http.closeWebSocket(this._connection); + } +} \ No newline at end of file From 6ea9e7de5deb3dc93a30a7f9b107322d07832e16 Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Mon, 6 Jul 2026 01:29:58 +0300 Subject: [PATCH 05/23] Replace shared connection Map with per-connection userData --- WebPlatform/server.alusus | 37 ++++++++++--------------------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/WebPlatform/server.alusus b/WebPlatform/server.alusus index 2fd5144..d68f05e 100644 --- a/WebPlatform/server.alusus +++ b/WebPlatform/server.alusus @@ -595,7 +595,7 @@ // Helpers // refer to element to an AST element using its pointer instead of referring to that element using an identifier - // makes generating generating code dynamically easier + // makes generating code dynamically easier func constructElementFullReference (element: ref[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { return Core.Basic.newSrdObj[Core.Data.Ast.Passage].{ Core.Basic.BindingOf[this].setMember("target", element); @@ -714,13 +714,13 @@ return Spp.astMgr.buildAst( ast { func (connection: ptr[Http.Connection], userData: ptr[Void]) : Void { - def connectionsMap: ref[Map[ArchInt, SrdRef[wsConnection]]]; - connectionsMap~no_deref = userData~cast[ptr[ServerSession]]~cnt.socketConnections; - def ws: SrdRef[wsConnection]; ws.alloc(); ws.obj~init(connection); - connectionsMap.set(connection~cast[ArchInt], ws); + + ws.refCounter.count++; + + Http.setUserConnectionData(connection, ws.refCounter~ptr~cast[ptr[RefCounter]]); providedHandlerFullRef(ws); } @@ -734,16 +734,8 @@ return Spp.astMgr.buildAst( ast { func (connection: ptr[Http.Connection], opcode: Int, data: CharsPtr, dataLen: ArchWord, userData: ptr[Void]): Int { - def connectionsMap: ref[Map[ArchInt, SrdRef[wsConnection]]]; - connectionsMap~no_deref = userData~cast[ptr[ServerSession]]~cnt.socketConnections; - - def pos: ArchInt = connectionsMap.findPos(connection~cast[ArchInt]); - - if (pos == -1) { - System.fail(1, "Data sent to already closed connection!"); - } - - def ws: SrdRef[wsConnection] = connectionsMap.valAt(pos); + def rc: ptr[RefCounter] = Http.getUserConnectionData(connection)~cast[ptr[RefCounter]]; + def ws: SrdRef[wsConnection](rc~cnt, rc~cnt.managedObj~cast[ptr[wsConnection]]~cnt); // opcode will be either 1 or 2 // 1 for text data @@ -753,7 +745,6 @@ providedHandlerFullRef(ws, data, dataLen, isBinary); // keep connection alive - // if the developer need to close the connection he can call the wsConnection.close() return 1; } @@ -767,19 +758,11 @@ return Spp.astMgr.buildAst( ast { func (connection: ptr[Http.Connection], userData: ptr[Void]): Void { - def connectionsMap: ref[Map[ArchInt, SrdRef[wsConnection]]]; - connectionsMap~no_deref = userData~cast[ptr[ServerSession]]~cnt.socketConnections; - - def pos: ArchInt = connectionsMap.findPos(connection~cast[ArchInt]); - if pos == -1 { - System.fail(1, "Tried to close an already closed connection!"); - } - def ws: SrdRef[wsConnection] = connectionsMap.valAt(pos); + def rc: ptr[RefCounter] = Http.getUserConnectionData(connection)~cast[ptr[RefCounter]]; + def ws: SrdRef[wsConnection](rc~cnt, rc~cnt.managedObj~cast[ptr[wsConnection]]~cnt); + ws.refCounter.count--; ws.dead(); - providedHandlerFullRef(ws); - - connectionsMap.removeAt(pos); } }, AstTemplateMap() From 66655c36ddb1022b4010bcf0f6db14fd0fb2c6ae Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Mon, 6 Jul 2026 23:23:25 +0300 Subject: [PATCH 06/23] refactor(websocket): declare endpoint handlers per-function instead of per-class Previously, a single annotated a class containing one method per handler Handlers are now declared individually, each with its own annotation on a standalone function --- WebPlatform/server.alusus | 171 ++++++++++++++++++++------------------ 1 file changed, 88 insertions(+), 83 deletions(-) diff --git a/WebPlatform/server.alusus b/WebPlatform/server.alusus index d68f05e..2e1e170 100644 --- a/WebPlatform/server.alusus +++ b/WebPlatform/server.alusus @@ -47,7 +47,6 @@ class ServerSession { def requestCallbackContext: SrdRef[RequestCallbackContext]; def httpContext: ptr[Http.Context]; - def socketConnections: Map[ArchInt, SrdRef[wsConnection]]; } def AstTemplateMap: alias Map[String, ref[Core.Basic.TiObject]]; @@ -232,18 +231,12 @@ } func generateBeEndpointsCalls (parent: ref[Core.Basic.TiObject]) { - def elements: Array[ref[Core.Basic.TiObject]] = Spp.astMgr.findElements( - ast { modifier == "beEndpoint" || modifier == "منفذ_بياني"}, - parent, - Spp.SeekerFlags.SKIP_OWNERS | Spp.SeekerFlags.SKIP_USES | Spp.SeekerFlags.SKIP_CHILDREN - ); + def elements : Array[ref[Core.Basic.TiObject]] = extractElementsWithGivenModifier(parent, "beEndpoint", "منفذ_بياني"); def i: Int; for i = 0, i < elements.getLength(), ++i { - def modifier: ref[Core.Basic.TiObject](Spp.astMgr.findModifierForElement(elements(i), "beEndpoint")); - if modifier~ptr == 0 modifier~no_deref = Spp.astMgr.findModifierForElement(elements(i), "منفذ_بياني"); def endpointParams: Array[String]; - if !Spp.astMgr.getModifierStringParams(modifier, endpointParams) - || endpointParams.getLength() < 2 { + extractModifiersParam(elements(i), "beEndpoint", "منفذ_بياني" , endpointParams); + if endpointParams.getLength() < 2 { System.fail(1, "Invalid BE endpoint params"); } Spp.astMgr.insertAst( @@ -306,82 +299,16 @@ } } - func generateWebSockets(parent: ref[Core.Basic.TiObject]) { - - // here we get all the classes with Ws modifier - - def classelEments: Array[ref[Core.Basic.TiObject]] = Spp.astMgr.findElements( - ast { modifier == "wsEndpoint" || modifier == "منفذ_مقبس" }, - parent, - Spp.SeekerFlags.SKIP_OWNERS | Spp.SeekerFlags.SKIP_USES | Spp.SeekerFlags.SKIP_CHILDREN - ); - - // here we will go inside each class to extract the callbacks - - def i: Int; - for i = 0, i < classelEments.getLength(), ++i { - - // here we extract the functions inside class - def handlerElements : Array[ref[Core.Basic.TiObject]] = Spp.astMgr.findElements ( - ast { elementType == "function" }, - classelEments(i), - Spp.SeekerFlags.SKIP_OWNERS | Spp.SeekerFlags.SKIP_USES | Spp.SeekerFlags.SKIP_CHILDREN - ); - - def onConnectElement: ref[Core.Basic.TiObject]; - def onReadyElement: ref[Core.Basic.TiObject]; - def onReceiveElement: ref[Core.Basic.TiObject]; - def onCloseElement: ref[Core.Basic.TiObject]; + function generateWebSockets(modulesRef: ref[Core.Basic.TiObject]) { + def webSocketHandlerElements : Array[ref[Core.Basic.TiObject]] = extractElementsWithGivenModifier(modulesRef, "wsEndpoint", "منفذ_مقبس"); - def j: Int; - for j = 0, j < handlerElements.getLength(), ++j { - def name: String = Spp.astMgr.getDefinitionName(handlerElements(j)); - if name == "onConnect" || name == "عندالأتصال" { - onConnectElement~no_deref = handlerElements(j); - } else if name == "onReady" || name == "عندالأستعداد" { - onReadyElement~no_deref = handlerElements(j); - } else if name == "onReceive" || name == "عند_الأستلام" { - onReceiveElement~no_deref = handlerElements(j); - } else if name == "onClose" || name == "عند_الاغلاق" { - onCloseElement~no_deref = handlerElements(j); - } else { - System.fail(1, "Invalid WS handler function name"); - } - } - if onConnectElement~ptr == 0 || onReadyElement~ptr == 0 - || onReceiveElement~ptr == 0 || onCloseElement~ptr == 0 { - System.fail(1, "Missing one or more WS handler functions"); - } + def webSocketEndpointsMap : Map[String , Map[String , ref[Core.Basic.TiObject]]](); + classifyWebsocketHandlersWithEndpoint(webSocketHandlerElements , webSocketEndpointsMap); - def modifier: ref[Core.Basic.TiObject](Spp.astMgr.findModifierForElement(classelEments(i), "wsEndpoint")); - if modifier~ptr == 0 modifier~no_deref = Spp.astMgr.findModifierForElement(classelEments(i), "منفذ_مقبس"); - - def wsEndpointParam: Array[String]; - if !Spp.astMgr.getModifierStringParams(modifier, wsEndpointParam) - || wsEndpointParam.getLength() != 1 { - System.fail(1, "Invalid WS endpoint param"); - } - - Spp.astMgr.insertAst( - ast { - Http.setWebSocketHandler( - httpContext, - "{{wsEndpointUri}}", - connectHandler, - readyHandler, - dataHandler, - closeHandler, - session - )}, - AstTemplateMap() - .set(Srl.String("wsEndpointUri"), Core.Basic.TiStr(wsEndpointParam(0))) - .set(Srl.String("connectHandler"), firstChildOf(constructSocketConnectHandler(onConnectElement))) - .set(Srl.String("readyHandler"), firstChildOf(constructSocketReadyHandler(onReadyElement))) - .set(Srl.String("dataHandler"), firstChildOf(constructSocketDataHandler(onReceiveElement))) - .set(Srl.String("closeHandler"), firstChildOf(constructSocketCloseHandler(onCloseElement))) - ); + def i : Int; + for i = 0 , i < webSocketEndpointsMap.getLength(), i++ { + insertWebSocketHandler(webSocketEndpointsMap.keyAt(i) , webSocketEndpointsMap.valAt(i)) } - } // Querying Functions @@ -594,6 +521,22 @@ // Helpers + function extractElementsWithGivenModifier (modulesRef: ref[Core.Basic.TiObject], enName : CharsPtr, arName : CharsPtr) : Array[ref[Core.Basic.TiObject]] { + return Spp.astMgr.findElements( + ast { modifier == enName || modifier == arName }, + modulesRef, + Spp.SeekerFlags.SKIP_OWNERS | Spp.SeekerFlags.SKIP_USES | Spp.SeekerFlags.SKIP_CHILDREN + ); + } + + function extractModifiersParam (elementRef : ref[Core.Basic.TiObject], enName : CharsPtr, arName : CharsPtr, Params : Array[String]){ + + def modifier: ref[Core.Basic.TiObject](Spp.astMgr.findModifierForElement(elementRef, enName)); + if modifier~ptr == 0 modifier~no_deref = Spp.astMgr.findModifierForElement(elementRef, arName); + + Spp.astMgr.getModifierStringParams(modifier, Params); + } + // refer to element to an AST element using its pointer instead of referring to that element using an identifier // makes generating code dynamically easier func constructElementFullReference (element: ref[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { @@ -769,5 +712,67 @@ .set(Srl.String("providedHandlerFullRef"), constructElementFullReference(element)) ); } + + function classifyWebsocketHandlersWithEndpoint (handlersRef : Array[ref[Core.Basic.TiObject]] , webSocketEndpointsMap : ref[Map[String , Map[String , ref[Core.Basic.TiObject]]]]) { + def index : Int; + for index = 0 , index < handlersRef.getLength() , index++ { + // extract modifiers + def modifierParam : Array[String]; + extractModifiersParam(handlersRef(index), "wsEndpoint", "منفذ_مقبس", modifierParam); + + if (modifierParam.getLength() == 0) { + System.fail(1, "Invalid params number for Ws modifier"); + } + + // set url + def socketUrl: String = modifierParam(1); + + def socketUrlHandlers: ref[Map[String, ref[Core.Basic.TiObject]]] = webSocketEndpointsMap(socketUrl); + + if socketUrlHandlers.getLength() == 0 { + socketUrlHandlers(String("onConnect")); + socketUrlHandlers(String("onReady")); + socketUrlHandlers(String("onData")); + socketUrlHandlers(String("onClose")); + } + + // set handler + def handlerName: String = modifierParam(2); + + def hPos: ArchInt = socketUrlHandlers.findPos(handlerName); + + if hPos != -1 { + socketUrlHandlers.setAt(hPos, handlersRef(index)~no_deref); + } + + } + } + + function insertWebSocketHandler (url : ref[String] , handlersElement : ref[Map[String , ref[Core.Basic.TiObject]]]) { + + def connectHandlerAstBlock : SrdRef[Core.Basic.TiObject] = constructSocketConnectHandler(handlersElement(String("onConnect"))); + def readyHandlerAstBlock : SrdRef[Core.Basic.TiObject] = constructSocketReadyHandler(handlersElement(String("onReady"))); + def dataHandlerAstBlock : SrdRef[Core.Basic.TiObject] = constructSocketDataHandler(handlersElement(String("onData"))); + def closeHandlerAstBlock : SrdRef[Core.Basic.TiObject] = constructSocketCloseHandler(handlersElement(String("onClose"))); + + Spp.astMgr.insertAst( + ast { + Http.setWebSocketHandler( + httpContext, + "{{wsEndpointUrl}}", + connectHandler, + readyHandler, + dataHandler, + closeHandler, + session + )}, + AstTemplateMap() + .set(Srl.String("wsEndpointUrl"), Core.Basic.TiStr(url)) + .set(Srl.String("connectHandler"), firstChildOf(connectHandlerAstBlock)) + .set(Srl.String("readyHandler"), firstChildOf(readyHandlerAstBlock)) + .set(Srl.String("dataHandler"), firstChildOf(dataHandlerAstBlock)) + .set(Srl.String("closeHandler"), firstChildOf(closeHandlerAstBlock)) + ); + } } From df16361a29150e1fed5ce02367be6190705f311e Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Tue, 7 Jul 2026 12:34:37 +0300 Subject: [PATCH 07/23] map string to ptr -- map[String , ptr[TiObject]] -- instead of mapping string to ref -- map[String , ptr[TiObject]] -- . --- WebPlatform/server.alusus | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/WebPlatform/server.alusus b/WebPlatform/server.alusus index 2e1e170..1ee2145 100644 --- a/WebPlatform/server.alusus +++ b/WebPlatform/server.alusus @@ -249,7 +249,7 @@ AstTemplateMap() .set(Srl.String("endpointMethod"), Core.Basic.TiStr(endpointParams(0))) .set(Srl.String("endpointUri"), Core.Basic.TiStr(endpointParams(1))) - .set(Srl.String("fullref"), constructElementFullReference(elements(i))) + .set(Srl.String("fullref"), constructElementFullReference(elements(i)~ptr)) ); } } @@ -283,7 +283,7 @@ def wsEndpointParam: Array[String]; if !Spp.astMgr.getModifierStringParams(modifier, wsEndpointParam) - || wsEndpointParam.getLength() != 1 { + || wsEndpointParam.getLength() != 2 { System.fail(1, "Invalid WS endpoint params"); } @@ -302,7 +302,7 @@ function generateWebSockets(modulesRef: ref[Core.Basic.TiObject]) { def webSocketHandlerElements : Array[ref[Core.Basic.TiObject]] = extractElementsWithGivenModifier(modulesRef, "wsEndpoint", "منفذ_مقبس"); - def webSocketEndpointsMap : Map[String , Map[String , ref[Core.Basic.TiObject]]](); + def webSocketEndpointsMap : Map[String , Map[String , ptr[Core.Basic.TiObject]]](); classifyWebsocketHandlersWithEndpoint(webSocketHandlerElements , webSocketEndpointsMap); def i : Int; @@ -539,9 +539,11 @@ // refer to element to an AST element using its pointer instead of referring to that element using an identifier // makes generating code dynamically easier - func constructElementFullReference (element: ref[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { + func constructElementFullReference (element: ptr[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { + def elementRef : ref[Core.Basic.TiObject]; + elementRef~ptr = element; return Core.Basic.newSrdObj[Core.Data.Ast.Passage].{ - Core.Basic.BindingOf[this].setMember("target", element); + Core.Basic.BindingOf[this].setMember("target", elementRef); }; } @@ -640,7 +642,7 @@ } // return block element with the wrapper function element as its first child - func constructSocketConnectHandler (element: ref[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { + func constructSocketConnectHandler (element: ptr[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { return Spp.astMgr.buildAst( ast { func (connection: ptr[Http.Connection], userData: ptr[Void]) : Int { @@ -653,7 +655,7 @@ ); } // return block element with the wrapper function element as its first child - func constructSocketReadyHandler (element: ref[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { + func constructSocketReadyHandler (element: ptr[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { return Spp.astMgr.buildAst( ast { func (connection: ptr[Http.Connection], userData: ptr[Void]) : Void { @@ -673,7 +675,7 @@ ) } // return block element with the wrapper function element as its first child - func constructSocketDataHandler(element: ref[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { + func constructSocketDataHandler(element: ptr[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { return Spp.astMgr.buildAst( ast { func (connection: ptr[Http.Connection], opcode: Int, data: CharsPtr, dataLen: ArchWord, userData: ptr[Void]): Int { @@ -697,7 +699,7 @@ ); } // return block element with the wrapper function element as its first child - func constructSocketCloseHandler(element: ref[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { + func constructSocketCloseHandler(element: ptr[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { return Spp.astMgr.buildAst( ast { func (connection: ptr[Http.Connection], userData: ptr[Void]): Void { @@ -713,7 +715,7 @@ ); } - function classifyWebsocketHandlersWithEndpoint (handlersRef : Array[ref[Core.Basic.TiObject]] , webSocketEndpointsMap : ref[Map[String , Map[String , ref[Core.Basic.TiObject]]]]) { + function classifyWebsocketHandlersWithEndpoint (handlersRef : Array[ref[Core.Basic.TiObject]] , webSocketEndpointsMap : ref[Map[String , Map[String , ptr[Core.Basic.TiObject]]]]) { def index : Int; for index = 0 , index < handlersRef.getLength() , index++ { // extract modifiers @@ -726,8 +728,9 @@ // set url def socketUrl: String = modifierParam(1); - - def socketUrlHandlers: ref[Map[String, ref[Core.Basic.TiObject]]] = webSocketEndpointsMap(socketUrl); + Console.print("this is the url : %s" , socketUrl.buf); + def socketUrlHandlers: ref[Map[String, ptr[Core.Basic.TiObject]]]; + socketUrlHandlers~ptr= webSocketEndpointsMap(socketUrl)~ptr; if socketUrlHandlers.getLength() == 0 { socketUrlHandlers(String("onConnect")); @@ -742,14 +745,15 @@ def hPos: ArchInt = socketUrlHandlers.findPos(handlerName); if hPos != -1 { - socketUrlHandlers.setAt(hPos, handlersRef(index)~no_deref); + def element : ptr[Core.Basic.TiObject] = handlersRef(index)~ptr; + + socketUrlHandlers.setAt(hPos, element); } } } - function insertWebSocketHandler (url : ref[String] , handlersElement : ref[Map[String , ref[Core.Basic.TiObject]]]) { - + function insertWebSocketHandler (url : ref[String] , handlersElement : ref[Map[String , ptr[Core.Basic.TiObject]]]) { def connectHandlerAstBlock : SrdRef[Core.Basic.TiObject] = constructSocketConnectHandler(handlersElement(String("onConnect"))); def readyHandlerAstBlock : SrdRef[Core.Basic.TiObject] = constructSocketReadyHandler(handlersElement(String("onReady"))); def dataHandlerAstBlock : SrdRef[Core.Basic.TiObject] = constructSocketDataHandler(handlersElement(String("onData"))); From 39a8fee1da71d0b4504438772dd0ce98f79dfc2f Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Tue, 7 Jul 2026 22:29:29 +0300 Subject: [PATCH 08/23] correct sume bugs in function extractElementsWithGivenModifier inside server.alusus --- WebPlatform/server.alusus | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/WebPlatform/server.alusus b/WebPlatform/server.alusus index 1ee2145..221e63a 100644 --- a/WebPlatform/server.alusus +++ b/WebPlatform/server.alusus @@ -521,20 +521,26 @@ // Helpers - function extractElementsWithGivenModifier (modulesRef: ref[Core.Basic.TiObject], enName : CharsPtr, arName : CharsPtr) : Array[ref[Core.Basic.TiObject]] { + function extractElementsWithGivenModifier(modulesRef: ref[Core.Basic.TiObject], enName: CharsPtr, arName: CharsPtr): Array[ref[Core.Basic.TiObject]] { + def translations: Map[String, String]; + translations.set(String(arName), String(enName)); + return Spp.astMgr.findElements( - ast { modifier == enName || modifier == arName }, + ast { elementType == "function" }, modulesRef, - Spp.SeekerFlags.SKIP_OWNERS | Spp.SeekerFlags.SKIP_USES | Spp.SeekerFlags.SKIP_CHILDREN + Spp.SeekerFlags.SKIP_OWNERS | Spp.SeekerFlags.SKIP_USES | Spp.SeekerFlags.SKIP_CHILDREN, + enName, + translations ); } - function extractModifiersParam (elementRef : ref[Core.Basic.TiObject], enName : CharsPtr, arName : CharsPtr, Params : Array[String]){ + function extractModifiersParam (elementRef : ref[Core.Basic.TiObject], enName : CharsPtr, arName : CharsPtr, params : ref[Array[String]]){ - def modifier: ref[Core.Basic.TiObject](Spp.astMgr.findModifierForElement(elementRef, enName)); - if modifier~ptr == 0 modifier~no_deref = Spp.astMgr.findModifierForElement(elementRef, arName); - - Spp.astMgr.getModifierStringParams(modifier, Params); + def translations: Map[String, String]; + translations.set(String(arName), String(enName)); + + def modifier: ref[Core.Basic.TiObject](Spp.astMgr.findModifierForElement(elementRef, enName,translations)); + Spp.astMgr.getModifierStringParams(modifier, params); } // refer to element to an AST element using its pointer instead of referring to that element using an identifier @@ -727,8 +733,8 @@ } // set url - def socketUrl: String = modifierParam(1); - Console.print("this is the url : %s" , socketUrl.buf); + def socketUrl: String = modifierParam(0); + def socketUrlHandlers: ref[Map[String, ptr[Core.Basic.TiObject]]]; socketUrlHandlers~ptr= webSocketEndpointsMap(socketUrl)~ptr; @@ -740,16 +746,16 @@ } // set handler - def handlerName: String = modifierParam(2); - - def hPos: ArchInt = socketUrlHandlers.findPos(handlerName); + def handlerName: String = modifierParam(1); - if hPos != -1 { - def element : ptr[Core.Basic.TiObject] = handlersRef(index)~ptr; + def hPos: ArchInt = socketUrlHandlers.findPos(handlerName); - socketUrlHandlers.setAt(hPos, element); + if hPos == -1 { + System.fail(1, "Wrong param for wsEndpoint modifier"); } + def element : ptr[Core.Basic.TiObject] = handlersRef(index)~ptr; + socketUrlHandlers.setAt(hPos, element); } } From ac575d04add463ba092fd25d4a6af8467462caa4 Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Thu, 9 Jul 2026 18:34:26 +0300 Subject: [PATCH 09/23] feat(websocket): make onConnect/onReady/onData/onClose handlers optional --- WebPlatform/server.alusus | 154 +++++++++++++++++++++++++------------- 1 file changed, 100 insertions(+), 54 deletions(-) diff --git a/WebPlatform/server.alusus b/WebPlatform/server.alusus index 221e63a..10bdb6e 100644 --- a/WebPlatform/server.alusus +++ b/WebPlatform/server.alusus @@ -647,21 +647,67 @@ } } - // return block element with the wrapper function element as its first child - func constructSocketConnectHandler (element: ptr[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { + func buildConnectBlock (element: ptr[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { + if element == null { + return Spp.astMgr.buildAst(ast { }, AstTemplateMap()); + } + return Spp.astMgr.buildAst( + ast { output = providedHandlerFullRef(connection); }, + AstTemplateMap().set(Srl.String("providedHandlerFullRef"), constructElementFullReference(element)) + ); + } + + func buildReadyBlock (element: ptr[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { + if element == null { + return Spp.astMgr.buildAst(ast { }, AstTemplateMap()); + } return Spp.astMgr.buildAst( + ast { providedHandlerFullRef(ws); }, + AstTemplateMap().set(Srl.String("providedHandlerFullRef"), constructElementFullReference(element)) + ); + } + + func buildDataBlock (element: ptr[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { + if element == null { + return Spp.astMgr.buildAst(ast { }, AstTemplateMap()); + } + return Spp.astMgr.buildAst( + ast { + def rc: ptr[RefCounter] = Http.getUserConnectionData(connection)~cast[ptr[RefCounter]]; + def ws: SrdRef[wsConnection](rc~cnt, rc~cnt.managedObj~cast[ptr[wsConnection]]~cnt); + providedHandlerFullRef(ws, data, dataLen, isBinary); }, + AstTemplateMap().set(Srl.String("providedHandlerFullRef"), constructElementFullReference(element)) + ); + } + + func buildCloseBlock (element: ptr[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { + if element == null { + return Spp.astMgr.buildAst(ast { }, AstTemplateMap()); + } + return Spp.astMgr.buildAst( + ast { providedHandlerFullRef(ws); }, + AstTemplateMap().set(Srl.String("providedHandlerFullRef"), constructElementFullReference(element)) + ); + } + + // return block element with the wrapper function element as its first child + func constructSocketConnectHandler (element: ptr[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { + def block: SrdRef[Core.Basic.TiObject] = buildConnectBlock(element); + return Spp.astMgr.buildAst( ast { - func (connection: ptr[Http.Connection], userData: ptr[Void]) : Int { - def output : Int = providedHandlerFullRef(connection); + func (connection: ptr[Http.Connection], userData: ptr[Void]): Int { + def output: Int = 0; + block; return output; } }, - AstTemplateMap() - .set(Srl.String("providedHandlerFullRef"), constructElementFullReference(element)) + AstTemplateMap().set(Srl.String("block"), block) ); } + // return block element with the wrapper function element as its first child func constructSocketReadyHandler (element: ptr[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { + def block : SrdRef[Core.Basic.TiObject] = buildReadyBlock(element); return Spp.astMgr.buildAst( ast { func (connection: ptr[Http.Connection], userData: ptr[Void]) : Void { @@ -673,55 +719,52 @@ Http.setUserConnectionData(connection, ws.refCounter~ptr~cast[ptr[RefCounter]]); - providedHandlerFullRef(ws); + block } }, AstTemplateMap() - .set(Srl.String("providedHandlerFullRef"), constructElementFullReference(element)) + .set(Srl.String("block"), block) ) } + // return block element with the wrapper function element as its first child - func constructSocketDataHandler(element: ptr[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { + func constructSocketDataHandler (element: ptr[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { + def block: SrdRef[Core.Basic.TiObject] = buildDataBlock(element); return Spp.astMgr.buildAst( - ast { - func (connection: ptr[Http.Connection], opcode: Int, data: CharsPtr, dataLen: ArchWord, userData: ptr[Void]): Int { - def rc: ptr[RefCounter] = Http.getUserConnectionData(connection)~cast[ptr[RefCounter]]; - def ws: SrdRef[wsConnection](rc~cnt, rc~cnt.managedObj~cast[ptr[wsConnection]]~cnt); - - // opcode will be either 1 or 2 - // 1 for text data - // 2 for binary data - - def isBinary: Bool = opcode == 2; - providedHandlerFullRef(ws, data, dataLen, isBinary); - - // keep connection alive - - return 1; - } - }, - AstTemplateMap() - .set(Srl.String("providedHandlerFullRef"), constructElementFullReference(element)) - ); + ast { + func (connection: ptr[Http.Connection], opcode: Int, data: CharsPtr, dataLen: ArchWord, userData: ptr[Void]): Int { + // opcode will be either 1 or 2 + // 1 for text data + // 2 for binary data + def isBinary: Bool = opcode == 2; + block; + // keep connection alive + return 1; + } + }, + AstTemplateMap().set(Srl.String("block"), block) + ); } // return block element with the wrapper function element as its first child - func constructSocketCloseHandler(element: ptr[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { + func constructSocketCloseHandler (element: ptr[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { + def block: SrdRef[Core.Basic.TiObject] = buildCloseBlock(element); return Spp.astMgr.buildAst( ast { func (connection: ptr[Http.Connection], userData: ptr[Void]): Void { def rc: ptr[RefCounter] = Http.getUserConnectionData(connection)~cast[ptr[RefCounter]]; def ws: SrdRef[wsConnection](rc~cnt, rc~cnt.managedObj~cast[ptr[wsConnection]]~cnt); - ws.refCounter.count--; ws.dead(); - providedHandlerFullRef(ws); + block; + ws.refCounter.count--; } }, - AstTemplateMap() - .set(Srl.String("providedHandlerFullRef"), constructElementFullReference(element)) + AstTemplateMap().set(Srl.String("block"), block) ); } - + function classifyWebsocketHandlersWithEndpoint (handlersRef : Array[ref[Core.Basic.TiObject]] , webSocketEndpointsMap : ref[Map[String , Map[String , ptr[Core.Basic.TiObject]]]]) { + def handlerPossibleNames : Array[String]({ String("onConnect"), String("onReady") , String("onData") , String("onClose") }); + def index : Int; for index = 0 , index < handlersRef.getLength() , index++ { // extract modifiers @@ -738,32 +781,36 @@ def socketUrlHandlers: ref[Map[String, ptr[Core.Basic.TiObject]]]; socketUrlHandlers~ptr= webSocketEndpointsMap(socketUrl)~ptr; - if socketUrlHandlers.getLength() == 0 { - socketUrlHandlers(String("onConnect")); - socketUrlHandlers(String("onReady")); - socketUrlHandlers(String("onData")); - socketUrlHandlers(String("onClose")); - } - // set handler def handlerName: String = modifierParam(1); - def hPos: ArchInt = socketUrlHandlers.findPos(handlerName); - - if hPos == -1 { - System.fail(1, "Wrong param for wsEndpoint modifier"); + def i : Int; + for i = 0 , i < handlerPossibleNames.getLength() , i++ { + if (handlerPossibleNames(i) == handlerName) { + socketUrlHandlers.set(handlerName, handlersRef(index)~ptr); + break; + } } - - def element : ptr[Core.Basic.TiObject] = handlersRef(index)~ptr; - socketUrlHandlers.setAt(hPos, element); } } function insertWebSocketHandler (url : ref[String] , handlersElement : ref[Map[String , ptr[Core.Basic.TiObject]]]) { - def connectHandlerAstBlock : SrdRef[Core.Basic.TiObject] = constructSocketConnectHandler(handlersElement(String("onConnect"))); - def readyHandlerAstBlock : SrdRef[Core.Basic.TiObject] = constructSocketReadyHandler(handlersElement(String("onReady"))); - def dataHandlerAstBlock : SrdRef[Core.Basic.TiObject] = constructSocketDataHandler(handlersElement(String("onData"))); - def closeHandlerAstBlock : SrdRef[Core.Basic.TiObject] = constructSocketCloseHandler(handlersElement(String("onClose"))); + def onConnect : ptr[Core.Basic.TiObject]; + if (handlersElement.findPos(String("onConnect")) == -1) onConnect = null else onConnect = handlersElement(String("onConnect")); + + def onReady : ptr[Core.Basic.TiObject]; + if (handlersElement.findPos(String("onReady")) == -1) onReady = null else onReady = handlersElement(String("onReady")); + + def onData : ptr[Core.Basic.TiObject]; + if (handlersElement.findPos(String("onData")) == -1) onData = null else onData = handlersElement(String("onData")); + + def onClose : ptr[Core.Basic.TiObject]; + if (handlersElement.findPos(String("onClose")) == -1) onClose = null else onClose = handlersElement(String("onClose")); + + def connectHandlerAstBlock : SrdRef[Core.Basic.TiObject] = constructSocketConnectHandler(onConnect); + def readyHandlerAstBlock : SrdRef[Core.Basic.TiObject] = constructSocketReadyHandler(onReady); + def dataHandlerAstBlock : SrdRef[Core.Basic.TiObject] = constructSocketDataHandler(onData); + def closeHandlerAstBlock : SrdRef[Core.Basic.TiObject] = constructSocketCloseHandler(onClose); Spp.astMgr.insertAst( ast { @@ -774,7 +821,6 @@ readyHandler, dataHandler, closeHandler, - session )}, AstTemplateMap() .set(Srl.String("wsEndpointUrl"), Core.Basic.TiStr(url)) From 3fb6208f80d490f99d738e84611915ca4436454b Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Fri, 10 Jul 2026 16:04:24 +0300 Subject: [PATCH 10/23] fix(websocket): remove trailing comma in setWebSocketHandler ast template --- WebPlatform/server.alusus | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebPlatform/server.alusus b/WebPlatform/server.alusus index 10bdb6e..8cddcc6 100644 --- a/WebPlatform/server.alusus +++ b/WebPlatform/server.alusus @@ -820,7 +820,7 @@ connectHandler, readyHandler, dataHandler, - closeHandler, + closeHandler )}, AstTemplateMap() .set(Srl.String("wsEndpointUrl"), Core.Basic.TiStr(url)) From 9d2d81b29b49e1287ebe628604934e344e73b5ed Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Fri, 10 Jul 2026 18:20:12 +0300 Subject: [PATCH 11/23] feat(websocket): add handshake info snapshot to wsConnection --- WebPlatform/web_socket_connection.alusus | 66 ++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/WebPlatform/web_socket_connection.alusus b/WebPlatform/web_socket_connection.alusus index 955e9d5..524102c 100644 --- a/WebPlatform/web_socket_connection.alusus +++ b/WebPlatform/web_socket_connection.alusus @@ -1,10 +1,76 @@ +function copyHandshake(info: ptr[Http.RequestInfo]): ref[HandshakeInfo] { + def result: ref[HandshakeInfo]; + result~ptr = Memory.alloc(HandshakeInfo~size)~cast[ptr[HandshakeInfo]]; + + result.requestMethod = String(info~cnt.requestMethod); + + result.requestUri = String(info~cnt.requestUri); + result.localUri = String(info~cnt.localUri); + if (info~cnt.queryString != null) + result.queryString = String(info~cnt.queryString) + else + result.queryString = String(""); + + result.remoteAddr = String(info~cnt.remoteAddr~ptr); + + result.remotePort = info~cnt.remotePort; + result.isSsl = info~cnt.isSsl; + + result.numberHeaders = info~cnt.numberHeaders; + result.headers.reserve(info~cnt.numberHeaders); + + def i : Int; + def h: HandshakeHeader; + for i = 0 , i < info~cnt.numberHeaders , i++ { + h.name = String(info~cnt.httpHeaders(i).name); + h.value = String(info~cnt.httpHeaders(i).value); + result.headers.add(h); + } + + return result; +} + +class HandshakeHeader { + def name: String; + def value: String; + handler this~init(){}; + handler this~init(src: ref[HandshakeHeader]) { + this.name = src.name; + this.value = src.value; + }; +}; + +class HandshakeInfo { + def requestMethod: String; + def requestUri: String; + def localUri: String; + def queryString: String; + def remoteAddr: String; + def remotePort: Int; + def isSsl: Int; + def headers: Array[HandshakeHeader]; + def numberHeaders: Int; + + handler this.getHeader(name: String): String { + def i : Int; + for i = 0 , i < this.headers.getLength() , i++{ + if (this.headers(i).name == name) { + return this.headers(i).value; + } + } + return String(""); + } +}; + class wsConnection { def _connection : ptr[Http.Connection]; + def handshakeInfo : ref[HandshakeInfo]; def isDead : Bool; handler this~init(connection: ptr[Http.Connection]) { this._connection = connection; this.isDead = 0; + this.handshakeInfo~ptr = copyHandshake(Http.getRequestInfo(connection))~ptr } handler this.dead () { From 5d079af799ee9dcd3e24dac915bb9365dd19519a Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Sat, 11 Jul 2026 23:58:09 +0300 Subject: [PATCH 12/23] feat(websocket): add maxMessageSize, fragmentation reassembly, and close-with-status-code --- WebPlatform/server.alusus | 80 +++++++++++++++++++++--- WebPlatform/web_socket_connection.alusus | 55 +++++++++++++++- 2 files changed, 124 insertions(+), 11 deletions(-) diff --git a/WebPlatform/server.alusus b/WebPlatform/server.alusus index 8cddcc6..24db86f 100644 --- a/WebPlatform/server.alusus +++ b/WebPlatform/server.alusus @@ -673,9 +673,8 @@ } return Spp.astMgr.buildAst( ast { - def rc: ptr[RefCounter] = Http.getUserConnectionData(connection)~cast[ptr[RefCounter]]; - def ws: SrdRef[wsConnection](rc~cnt, rc~cnt.managedObj~cast[ptr[wsConnection]]~cnt); - providedHandlerFullRef(ws, data, dataLen, isBinary); }, + providedHandlerFullRef(ws,ws.fragmentBuffer.string, isBinary); + }, AstTemplateMap().set(Srl.String("providedHandlerFullRef"), constructElementFullReference(element)) ); } @@ -732,13 +731,74 @@ def block: SrdRef[Core.Basic.TiObject] = buildDataBlock(element); return Spp.astMgr.buildAst( ast { - func (connection: ptr[Http.Connection], opcode: Int, data: CharsPtr, dataLen: ArchWord, userData: ptr[Void]): Int { - // opcode will be either 1 or 2 - // 1 for text data - // 2 for binary data - def isBinary: Bool = opcode == 2; - block; - // keep connection alive + func (connection: ptr[Http.Connection], bits: Int, data: CharsPtr, dataLen: ArchWord, userData: ptr[Void]): Int { + def rc: ptr[RefCounter] = Http.getUserConnectionData(connection)~cast[ptr[RefCounter]]; + def ws: SrdRef[wsConnection](rc~cnt, rc~cnt.managedObj~cast[ptr[wsConnection]]~cnt); + + def opcode : Int = bits & 0x0F; + def fin: Bool = (bits & 0x80) != 0; + // if the frame is Text or Binary + if opcode == 1 or opcode == 2 { + Console.print("we recieve a data\n") + if (ws.isFragmenting) { + ws.close(1002, "unexpected new message mid-fragment"); + Console.print("data_handler: close() returned, about to return 0\n") + return 0; + } + + ws.fragmentOpcode = opcode; + ws.fragmentBuffer.clear(); + + if (dataLen~cast[ArchInt] > ws.getMaxMessageSize()) { + ws.close(1009, "Message too big"); + return 0; + } + + ws.fragmentBuffer.append(data, dataLen~cast[ArchInt]); + Console.print("the data now is inside the buffer. the data is : %s\n" , ws.fragmentBuffer.string.buf) + if (fin) { + Console.print("we about to call the handler\n") + def isBinary : Bool = ws.fragmentOpcode == 2; + block; + ws.fragmentBuffer.clear(); + } else { + ws.isFragmenting = 1; + } + } + + if opcode == 0 { + + if (!ws.isFragmenting) { + // Protocol violation: CONTINUATION with no preceding TEXT/BINARY + ws.close(1002, "unexpected continuation frame"); + return 0; + } + + if (ws.fragmentBuffer.getLength() + dataLen > ws.getMaxMessageSize()) { + ws.close(1009, "Message too big"); + return 0; + } + + ws.fragmentBuffer.append(data, dataLen); + + if (fin) { + def isBinary : Bool = ws.fragmentOpcode == 2; + block; + ws.fragmentBuffer.clear(); + ws.isFragmenting = 0; + } + } + + if opcode == 8 { + // reply with a close frame before tearing down — completes the handshake properly + if dataLen~cast[Int] >= 2 { + ws.close_raw_echo(data, dataLen); // echo back client's code+reason + } else { + ws.close(1000, ""); // client sent no code, reply with normal closure + } + return 0; + } + return 1; } }, diff --git a/WebPlatform/web_socket_connection.alusus b/WebPlatform/web_socket_connection.alusus index 524102c..c0e4417 100644 --- a/WebPlatform/web_socket_connection.alusus +++ b/WebPlatform/web_socket_connection.alusus @@ -66,6 +66,12 @@ class wsConnection { def _connection : ptr[Http.Connection]; def handshakeInfo : ref[HandshakeInfo]; def isDead : Bool; + def _maxMessageSize : ArchInt = 1024; + + // fragmentation state + def fragmentBuffer : StringBuilder(); + def fragmentOpcode : Int; + def isFragmenting : Bool; handler this~init(connection: ptr[Http.Connection]) { this._connection = connection; @@ -73,6 +79,15 @@ class wsConnection { this.handshakeInfo~ptr = copyHandshake(Http.getRequestInfo(connection))~ptr } + handler this.setMaxMessageSize (size : ArchInt) { + _maxMessageSize = size; + fragmentBuffer.bufferGrowSize = (_maxMessageSize / 4)~cast[ArchInt]; + } + + handler this.getMaxMessageSize() : ArchInt{ + return this._maxMessageSize; + } + handler this.dead () { this._connection = 0; this.isDead = 1; @@ -93,8 +108,46 @@ class wsConnection { return Http.writeBinaryToWebSocket(this._connection , data , dataLen); } + // close one with no status code and reason from user + // this will closed with status code 1000 handler this.close () : Int { + return this.close(1000 , ""); + } + + handler this.close (statusCode : word[16] , reasonMessage : CharsPtr) : Int { if this.isDead return 0; - return Http.closeWebSocket(this._connection); + + def reason_len : Int = String(reasonMessage).getLength(); + + def payload_len : Int = 2 + reason_len; + + if (payload_len > 125) { + reason_len = 123; // 125 - 2 bytes for the status code + payload_len = 125; + } + + // close frame payload max is 125 bytes + def payload : array[Char, 125]; + + payload(0) = (statusCode >> 8) & 0xFF; // status code, high byte + payload(1) = statusCode & 0xFF; // status code, low byte + + if (reason_len > 0) { + Memory.copy(payload~ptr + 2, reasonMessage, reason_len); + } + //Console.print("this is the reasonMessage : %s\n" , String(reasonMessage).buf) + //Console.print("close: about to write, payload_len=%i\n", payload_len) + + //Console.print("this is the message %i\n" , String(payload~ptr ,payload_len).getLength()); + + def result : Int = Http.writeToWebSocket(this._connection, 8, payload~ptr, payload_len); + + this.dead() + + return result; + } + + handler this.close_raw_echo (data : CharsPtr , dataLen : ArchWord) : Int { + return Http.writeToWebSocket(this._connection, 8, data, dataLen); } } \ No newline at end of file From ca99d385597bd0125792db18163bc3d9f412b3f9 Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Sun, 12 Jul 2026 00:54:27 +0300 Subject: [PATCH 13/23] (websoxket) Capitalize class name from ws_connection to Ws_connection --- WebPlatform/server.alusus | 6 +++--- WebPlatform/web_socket_connection.alusus | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/WebPlatform/server.alusus b/WebPlatform/server.alusus index 24db86f..e7f89bc 100644 --- a/WebPlatform/server.alusus +++ b/WebPlatform/server.alusus @@ -710,7 +710,7 @@ return Spp.astMgr.buildAst( ast { func (connection: ptr[Http.Connection], userData: ptr[Void]) : Void { - def ws: SrdRef[wsConnection]; + def ws: SrdRef[WsConnection]; ws.alloc(); ws.obj~init(connection); @@ -733,7 +733,7 @@ ast { func (connection: ptr[Http.Connection], bits: Int, data: CharsPtr, dataLen: ArchWord, userData: ptr[Void]): Int { def rc: ptr[RefCounter] = Http.getUserConnectionData(connection)~cast[ptr[RefCounter]]; - def ws: SrdRef[wsConnection](rc~cnt, rc~cnt.managedObj~cast[ptr[wsConnection]]~cnt); + def ws: SrdRef[WsConnection](rc~cnt, rc~cnt.managedObj~cast[ptr[WsConnection]]~cnt); def opcode : Int = bits & 0x0F; def fin: Bool = (bits & 0x80) != 0; @@ -812,7 +812,7 @@ ast { func (connection: ptr[Http.Connection], userData: ptr[Void]): Void { def rc: ptr[RefCounter] = Http.getUserConnectionData(connection)~cast[ptr[RefCounter]]; - def ws: SrdRef[wsConnection](rc~cnt, rc~cnt.managedObj~cast[ptr[wsConnection]]~cnt); + def ws: SrdRef[WsConnection](rc~cnt, rc~cnt.managedObj~cast[ptr[WsConnection]]~cnt); ws.dead(); block; ws.refCounter.count--; diff --git a/WebPlatform/web_socket_connection.alusus b/WebPlatform/web_socket_connection.alusus index c0e4417..324f44b 100644 --- a/WebPlatform/web_socket_connection.alusus +++ b/WebPlatform/web_socket_connection.alusus @@ -62,7 +62,7 @@ class HandshakeInfo { } }; -class wsConnection { +class WsConnection { def _connection : ptr[Http.Connection]; def handshakeInfo : ref[HandshakeInfo]; def isDead : Bool; From 76c7679df1133e90ad15d5faddfbc1679a4bd64e Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Wed, 15 Jul 2026 00:20:47 +0300 Subject: [PATCH 14/23] (websocket) : return back to classes --- WebPlatform/server.alusus | 291 ++++++----------------- WebPlatform/web_socket_connection.alusus | 200 ++++++++++++---- 2 files changed, 227 insertions(+), 264 deletions(-) diff --git a/WebPlatform/server.alusus b/WebPlatform/server.alusus index e7f89bc..bfdf5cd 100644 --- a/WebPlatform/server.alusus +++ b/WebPlatform/server.alusus @@ -231,7 +231,7 @@ } func generateBeEndpointsCalls (parent: ref[Core.Basic.TiObject]) { - def elements : Array[ref[Core.Basic.TiObject]] = extractElementsWithGivenModifier(parent, "beEndpoint", "منفذ_بياني"); + def elements : Array[ref[Core.Basic.TiObject]] = extractFunctionElements(parent, "beEndpoint", "منفذ_بياني"); def i: Int; for i = 0, i < elements.getLength(), ++i { def endpointParams: Array[String]; @@ -283,10 +283,9 @@ def wsEndpointParam: Array[String]; if !Spp.astMgr.getModifierStringParams(modifier, wsEndpointParam) - || wsEndpointParam.getLength() != 2 { + || wsEndpointParam.getLength() != 1 { System.fail(1, "Invalid WS endpoint params"); } - Spp.astMgr.insertAst( ast { if String.isEqual(method, "GET") && String.isEqual(uri, "{{wsEndpointUri}}") { @@ -300,14 +299,62 @@ } function generateWebSockets(modulesRef: ref[Core.Basic.TiObject]) { - def webSocketHandlerElements : Array[ref[Core.Basic.TiObject]] = extractElementsWithGivenModifier(modulesRef, "wsEndpoint", "منفذ_مقبس"); + def webSocketClasses : Array[ref[Core.Basic.TiObject]] = extractTypeElements(modulesRef, "wsEndpoint", "منفذ_مقبس"); + + // register a single set of free-function trampolines that forward each + // callback to the matching bound method on the instance stored in userData, + // since civetweb's C callbacks can't carry an instance pointer directly - def webSocketEndpointsMap : Map[String , Map[String , ptr[Core.Basic.TiObject]]](); - classifyWebsocketHandlersWithEndpoint(webSocketHandlerElements , webSocketEndpointsMap); + if (webSocketClasses.getLength() > 0) { + Spp.astMgr.insertAst( + ast { + func trampolineConnection (connection: ptr[Http.Connection], userData: ptr[Void]): Int { + def instance: ptr[WsRoute] = userData~cast[ptr[WsRoute]]; + return instance~cnt.onConnectWrapper(connection, userData); + } + func trampolineReady (connection: ptr[Http.Connection], userData: ptr[Void]): Void { + def instance: ptr[WsRoute] = userData~cast[ptr[WsRoute]]; + instance~cnt.onReadyWrapper(connection, userData); + } + func trampolineData (connection: ptr[Http.Connection], bits: Int, data: CharsPtr, dataLen: ArchWord, userData: ptr[Void]): Int { + def instance: ptr[WsRoute] = userData~cast[ptr[WsRoute]]; + return instance~cnt.onDataWrapper(connection, bits, data, dataLen, userData); + } + func trampolineClose (connection: ptr[Http.Connection], userData: ptr[Void]): Void { + def instance: ptr[WsRoute] = userData~cast[ptr[WsRoute]]; + instance~cnt.onCloseWrapper(connection, userData); + } + }, + AstTemplateMap() + ); + } def i : Int; - for i = 0 , i < webSocketEndpointsMap.getLength(), i++ { - insertWebSocketHandler(webSocketEndpointsMap.keyAt(i) , webSocketEndpointsMap.valAt(i)) + for i = 0 , i < webSocketClasses.getLength(), i++ { + def params : Array[String]; + extractModifiersParam(webSocketClasses(i) , "wsEndpoint", "منفذ_مقبس" , params); + + if (params.getLength() != 1) { + System.fail(1 , "where is the endpoint"); + } + + Spp.astMgr.insertAst( + ast { + def instance : webSocketClass; + instance.directTest() + Http.setWebSocketHandler( + httpContext, + "{{wsEndpointUri}}", + trampolineConnection~ptr, + trampolineReady~ptr, + trampolineData~ptr, + trampolineClose~ptr + ) + }, + AstTemplateMap() + .set(Srl.String("wsEndpointUri"), Core.Basic.TiStr(params(0))) + .set(Srl.String("webSocketClass"), webSocketClasses(i)) + ); } } // Querying Functions @@ -385,7 +432,6 @@ session~cnt~init(); session~cnt.requestCallbackContext = requestCallbackContext; session~cnt.httpContext = httpContext; - preprocess { def modules: Array[ref[Core.Basic.TiObject]] = getAllModules(modulesRef~ast); if modules.getLength() == 0 { @@ -521,10 +567,9 @@ // Helpers - function extractElementsWithGivenModifier(modulesRef: ref[Core.Basic.TiObject], enName: CharsPtr, arName: CharsPtr): Array[ref[Core.Basic.TiObject]] { + function extractFunctionElements (modulesRef: ref[Core.Basic.TiObject], enName: CharsPtr, arName: CharsPtr): Array[ref[Core.Basic.TiObject]] { def translations: Map[String, String]; translations.set(String(arName), String(enName)); - return Spp.astMgr.findElements( ast { elementType == "function" }, modulesRef, @@ -534,6 +579,17 @@ ); } + function extractTypeElements (modulesRef: ref[Core.Basic.TiObject], enName: CharsPtr, arName: CharsPtr): Array[ref[Core.Basic.TiObject]] { + def translations: Map[String, String]; + translations.set(String(arName), String(enName)); + return Spp.astMgr.findElements( + ast { elementType == "type" }, + modulesRef, + Spp.SeekerFlags.SKIP_OWNERS | Spp.SeekerFlags.SKIP_USES | Spp.SeekerFlags.SKIP_CHILDREN, + enName, + translations + ); + } function extractModifiersParam (elementRef : ref[Core.Basic.TiObject], enName : CharsPtr, arName : CharsPtr, params : ref[Array[String]]){ def translations: Map[String, String]; @@ -647,180 +703,7 @@ } } - func buildConnectBlock (element: ptr[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { - if element == null { - return Spp.astMgr.buildAst(ast { }, AstTemplateMap()); - } - return Spp.astMgr.buildAst( - ast { output = providedHandlerFullRef(connection); }, - AstTemplateMap().set(Srl.String("providedHandlerFullRef"), constructElementFullReference(element)) - ); - } - - func buildReadyBlock (element: ptr[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { - if element == null { - return Spp.astMgr.buildAst(ast { }, AstTemplateMap()); - } - return Spp.astMgr.buildAst( - ast { providedHandlerFullRef(ws); }, - AstTemplateMap().set(Srl.String("providedHandlerFullRef"), constructElementFullReference(element)) - ); - } - - func buildDataBlock (element: ptr[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { - if element == null { - return Spp.astMgr.buildAst(ast { }, AstTemplateMap()); - } - return Spp.astMgr.buildAst( - ast { - providedHandlerFullRef(ws,ws.fragmentBuffer.string, isBinary); - }, - AstTemplateMap().set(Srl.String("providedHandlerFullRef"), constructElementFullReference(element)) - ); - } - - func buildCloseBlock (element: ptr[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { - if element == null { - return Spp.astMgr.buildAst(ast { }, AstTemplateMap()); - } - return Spp.astMgr.buildAst( - ast { providedHandlerFullRef(ws); }, - AstTemplateMap().set(Srl.String("providedHandlerFullRef"), constructElementFullReference(element)) - ); - } - - // return block element with the wrapper function element as its first child - func constructSocketConnectHandler (element: ptr[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { - def block: SrdRef[Core.Basic.TiObject] = buildConnectBlock(element); - return Spp.astMgr.buildAst( - ast { - func (connection: ptr[Http.Connection], userData: ptr[Void]): Int { - def output: Int = 0; - block; - return output; - } - }, - AstTemplateMap().set(Srl.String("block"), block) - ); - } - - // return block element with the wrapper function element as its first child - func constructSocketReadyHandler (element: ptr[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { - def block : SrdRef[Core.Basic.TiObject] = buildReadyBlock(element); - return Spp.astMgr.buildAst( - ast { - func (connection: ptr[Http.Connection], userData: ptr[Void]) : Void { - def ws: SrdRef[WsConnection]; - ws.alloc(); - ws.obj~init(connection); - - ws.refCounter.count++; - - Http.setUserConnectionData(connection, ws.refCounter~ptr~cast[ptr[RefCounter]]); - - block - } - }, - AstTemplateMap() - .set(Srl.String("block"), block) - ) - } - - // return block element with the wrapper function element as its first child - func constructSocketDataHandler (element: ptr[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { - def block: SrdRef[Core.Basic.TiObject] = buildDataBlock(element); - return Spp.astMgr.buildAst( - ast { - func (connection: ptr[Http.Connection], bits: Int, data: CharsPtr, dataLen: ArchWord, userData: ptr[Void]): Int { - def rc: ptr[RefCounter] = Http.getUserConnectionData(connection)~cast[ptr[RefCounter]]; - def ws: SrdRef[WsConnection](rc~cnt, rc~cnt.managedObj~cast[ptr[WsConnection]]~cnt); - - def opcode : Int = bits & 0x0F; - def fin: Bool = (bits & 0x80) != 0; - // if the frame is Text or Binary - if opcode == 1 or opcode == 2 { - Console.print("we recieve a data\n") - if (ws.isFragmenting) { - ws.close(1002, "unexpected new message mid-fragment"); - Console.print("data_handler: close() returned, about to return 0\n") - return 0; - } - - ws.fragmentOpcode = opcode; - ws.fragmentBuffer.clear(); - - if (dataLen~cast[ArchInt] > ws.getMaxMessageSize()) { - ws.close(1009, "Message too big"); - return 0; - } - - ws.fragmentBuffer.append(data, dataLen~cast[ArchInt]); - Console.print("the data now is inside the buffer. the data is : %s\n" , ws.fragmentBuffer.string.buf) - if (fin) { - Console.print("we about to call the handler\n") - def isBinary : Bool = ws.fragmentOpcode == 2; - block; - ws.fragmentBuffer.clear(); - } else { - ws.isFragmenting = 1; - } - } - - if opcode == 0 { - - if (!ws.isFragmenting) { - // Protocol violation: CONTINUATION with no preceding TEXT/BINARY - ws.close(1002, "unexpected continuation frame"); - return 0; - } - - if (ws.fragmentBuffer.getLength() + dataLen > ws.getMaxMessageSize()) { - ws.close(1009, "Message too big"); - return 0; - } - - ws.fragmentBuffer.append(data, dataLen); - - if (fin) { - def isBinary : Bool = ws.fragmentOpcode == 2; - block; - ws.fragmentBuffer.clear(); - ws.isFragmenting = 0; - } - } - - if opcode == 8 { - // reply with a close frame before tearing down — completes the handshake properly - if dataLen~cast[Int] >= 2 { - ws.close_raw_echo(data, dataLen); // echo back client's code+reason - } else { - ws.close(1000, ""); // client sent no code, reply with normal closure - } - return 0; - } - - return 1; - } - }, - AstTemplateMap().set(Srl.String("block"), block) - ); - } - // return block element with the wrapper function element as its first child - func constructSocketCloseHandler (element: ptr[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { - def block: SrdRef[Core.Basic.TiObject] = buildCloseBlock(element); - return Spp.astMgr.buildAst( - ast { - func (connection: ptr[Http.Connection], userData: ptr[Void]): Void { - def rc: ptr[RefCounter] = Http.getUserConnectionData(connection)~cast[ptr[RefCounter]]; - def ws: SrdRef[WsConnection](rc~cnt, rc~cnt.managedObj~cast[ptr[WsConnection]]~cnt); - ws.dead(); - block; - ws.refCounter.count--; - } - }, - AstTemplateMap().set(Srl.String("block"), block) - ); - } + function classifyWebsocketHandlersWithEndpoint (handlersRef : Array[ref[Core.Basic.TiObject]] , webSocketEndpointsMap : ref[Map[String , Map[String , ptr[Core.Basic.TiObject]]]]) { def handlerPossibleNames : Array[String]({ String("onConnect"), String("onReady") , String("onData") , String("onClose") }); @@ -853,42 +736,4 @@ } } } - - function insertWebSocketHandler (url : ref[String] , handlersElement : ref[Map[String , ptr[Core.Basic.TiObject]]]) { - def onConnect : ptr[Core.Basic.TiObject]; - if (handlersElement.findPos(String("onConnect")) == -1) onConnect = null else onConnect = handlersElement(String("onConnect")); - - def onReady : ptr[Core.Basic.TiObject]; - if (handlersElement.findPos(String("onReady")) == -1) onReady = null else onReady = handlersElement(String("onReady")); - - def onData : ptr[Core.Basic.TiObject]; - if (handlersElement.findPos(String("onData")) == -1) onData = null else onData = handlersElement(String("onData")); - - def onClose : ptr[Core.Basic.TiObject]; - if (handlersElement.findPos(String("onClose")) == -1) onClose = null else onClose = handlersElement(String("onClose")); - - def connectHandlerAstBlock : SrdRef[Core.Basic.TiObject] = constructSocketConnectHandler(onConnect); - def readyHandlerAstBlock : SrdRef[Core.Basic.TiObject] = constructSocketReadyHandler(onReady); - def dataHandlerAstBlock : SrdRef[Core.Basic.TiObject] = constructSocketDataHandler(onData); - def closeHandlerAstBlock : SrdRef[Core.Basic.TiObject] = constructSocketCloseHandler(onClose); - - Spp.astMgr.insertAst( - ast { - Http.setWebSocketHandler( - httpContext, - "{{wsEndpointUrl}}", - connectHandler, - readyHandler, - dataHandler, - closeHandler - )}, - AstTemplateMap() - .set(Srl.String("wsEndpointUrl"), Core.Basic.TiStr(url)) - .set(Srl.String("connectHandler"), firstChildOf(connectHandlerAstBlock)) - .set(Srl.String("readyHandler"), firstChildOf(readyHandlerAstBlock)) - .set(Srl.String("dataHandler"), firstChildOf(dataHandlerAstBlock)) - .set(Srl.String("closeHandler"), firstChildOf(closeHandlerAstBlock)) - ); - } - } diff --git a/WebPlatform/web_socket_connection.alusus b/WebPlatform/web_socket_connection.alusus index 324f44b..3209fb4 100644 --- a/WebPlatform/web_socket_connection.alusus +++ b/WebPlatform/web_socket_connection.alusus @@ -62,60 +62,54 @@ class HandshakeInfo { } }; +class WsState { + @shared def CONNECTING : String("connecting"); + @shared def OPENED : String("opened"); + @shared def CLOSING : String("closing"); + @shared def CLOSED : String("closed"); +} + class WsConnection { def _connection : ptr[Http.Connection]; def handshakeInfo : ref[HandshakeInfo]; - def isDead : Bool; - def _maxMessageSize : ArchInt = 1024; - - // fragmentation state - def fragmentBuffer : StringBuilder(); - def fragmentOpcode : Int; - def isFragmenting : Bool; + def status : String; - handler this~init(connection: ptr[Http.Connection]) { - this._connection = connection; - this.isDead = 0; - this.handshakeInfo~ptr = copyHandshake(Http.getRequestInfo(connection))~ptr - } - - handler this.setMaxMessageSize (size : ArchInt) { - _maxMessageSize = size; - fragmentBuffer.bufferGrowSize = (_maxMessageSize / 4)~cast[ArchInt]; - } - - handler this.getMaxMessageSize() : ArchInt{ - return this._maxMessageSize; - } - - handler this.dead () { - this._connection = 0; - this.isDead = 1; + handler this~init (conn : ptr[Http.Connection]) { + this._connection = conn; + this.status = WsState.CONNECTING; } handler this.sendText (data: CharsPtr, dataLen: ArchWord) : Int { - if this.isDead return 0; + if this.status != WsState.OPENED return 0 ; return Http.writeTextToWebSocket(this._connection, data, dataLen); } handler this.sendText (data: CharsPtr) : Int { - if this.isDead return 0; + if this.status != WsState.OPENED return 0 ; return Http.writeTextToWebSocket(this._connection, data); } handler this.sendBinary (data: CharsPtr, dataLen: ArchWord) : Int { - if this.isDead return 0; + if this.status != WsState.OPENED return 0 ; return Http.writeBinaryToWebSocket(this._connection , data , dataLen); } - - // close one with no status code and reason from user - // this will closed with status code 1000 + handler this.close () : Int { return this.close(1000 , ""); } + // Called internally by the library to respond to a client-initiated + // close frame, completing the close handshake as stated in + // RFC 6455 §5.5.1 (a responder typically echoes the status code + // it received). + handler this.replyToClose (data : CharsPtr, dataLen : ArchWord) : Int { + if this.status != WsState.OPENED return 0 ; + def result : Int = Http.writeToWebSocket(this._connection, 8, data, dataLen); + return result; + } + handler this.close (statusCode : word[16] , reasonMessage : CharsPtr) : Int { - if this.isDead return 0; + if this.status != WsState.OPENED return 0 ; def reason_len : Int = String(reasonMessage).getLength(); @@ -126,7 +120,7 @@ class WsConnection { payload_len = 125; } - // close frame payload max is 125 bytes + // close frame payload max is 125 bytes as stated in RFC 6455 §5.5.1 def payload : array[Char, 125]; payload(0) = (statusCode >> 8) & 0xFF; // status code, high byte @@ -135,19 +129,143 @@ class WsConnection { if (reason_len > 0) { Memory.copy(payload~ptr + 2, reasonMessage, reason_len); } - //Console.print("this is the reasonMessage : %s\n" , String(reasonMessage).buf) - //Console.print("close: about to write, payload_len=%i\n", payload_len) - - //Console.print("this is the message %i\n" , String(payload~ptr ,payload_len).getLength()); def result : Int = Http.writeToWebSocket(this._connection, 8, payload~ptr, payload_len); - - this.dead() return result; } +} + +class WsRoute { + def _maxMessageSize : ArchInt = 1024; + + // fragmentation state + def fragmentBuffer : StringBuilder(); + def fragmentOpcode : Int; + def isFragmenting : Bool; + + handler this.onConnectWrapper (connection: ptr[Http.Connection], userData: ptr[Void]) : Int { + def ws: SrdRef[WsConnection]; + ws.alloc(); + ws.obj~init(connection); + + // we increment counter by one so we don't lose the object even if the user + // does not reference it + + ws.refCounter.count++; + Http.setUserConnectionData(connection, ws.refCounter~ptr~cast[ptr[RefCounter]]); + + return this.onConnect(ws); + } + handler this.onReadyWrapper (connection: ptr[Http.Connection], userData: ptr[Void]) : Void { + def rc: ptr[RefCounter] = Http.getUserConnectionData(connection)~cast[ptr[RefCounter]]; + def ws: SrdRef[WsConnection](rc~cnt, rc~cnt.managedObj~cast[ptr[WsConnection]]~cnt); + + ws.status = WsState.OPENED; + + this.onReady(ws); + } - handler this.close_raw_echo (data : CharsPtr , dataLen : ArchWord) : Int { - return Http.writeToWebSocket(this._connection, 8, data, dataLen); + handler this.onDataWrapper (connection: ptr[Http.Connection], bits: Int, data: CharsPtr, dataLen: ArchWord, userData: ptr[Void]) : Int { + def rc: ptr[RefCounter] = Http.getUserConnectionData(connection)~cast[ptr[RefCounter]]; + def ws: SrdRef[WsConnection](rc~cnt, rc~cnt.managedObj~cast[ptr[WsConnection]]~cnt); + + def opcode : Int = bits & 0x0F; + def fin: Bool = (bits & 0x80) != 0; + + // Safe to cast: CivetWeb rejects any frame exceeding ~2 GiB (0x7FFF0000), + // which is within ArchInt's range, so this cast can't overflow or go negative. + + def data_Len : ArchInt = dataLen~cast[ArchInt]; + + if opcode == 1 or opcode == 2 { + if (this.isFragmenting) { + ws.close(1002, "unexpected new message mid-fragment"); + return 0; + } + + this.fragmentOpcode = opcode; + this.fragmentBuffer.clear(); + + if (data_Len > this.getMaxMessageSize()) { + ws.close(1009, "Message too big"); + return 0; + } + + this.fragmentBuffer.append(data, data_Len); + + if (fin) { + def isBinary : Bool = this.fragmentOpcode == 2; + this.onData(ws , this.fragmentBuffer.string ,isBinary); + this.fragmentBuffer.clear(); + } else { + this.isFragmenting = 1; + } + } + + if opcode == 0 { + + if (!this.isFragmenting) { + // Protocol violation: CONTINUATION with no preceding TEXT/BINARY + ws.close(1002, "unexpected continuation frame"); + return 0; + } + + if (this.fragmentBuffer.getLength() + data_Len > this.getMaxMessageSize()) { + ws.close(1009, "Message too big"); + return 0; + } + + this.fragmentBuffer.append(data, data_Len); + + if (fin) { + def isBinary : Bool = this.fragmentOpcode == 2; + this.onData(ws , this.fragmentBuffer.string ,isBinary); + this.fragmentBuffer.clear(); + this.isFragmenting = 0; + } + } + + if opcode == 8 { + // reply with a close frame before tearing down — completes the handshake properly + if data_Len~cast[Int] >= 2 { + ws.replyToClose(data, dataLen); // echo back client's code+reason + } else { + ws.close(1000, ""); // client sent no code, reply with normal closure + } + return 0; + } + + return 1; + } + + handler this.onCloseWrapper (connection: ptr[Http.Connection], userData: ptr[Void]) : Void { + def rc: ptr[RefCounter] = Http.getUserConnectionData(connection)~cast[ptr[RefCounter]]; + def ws: SrdRef[WsConnection](rc~cnt, rc~cnt.managedObj~cast[ptr[WsConnection]]~cnt); + ws.status = WsState.CLOSED; + + this.onClose(ws); + + // reduce counter by one cause we was increment it by one when we initialize the reference to make sure that the object + // will not be released before we closing the connection + + ws.refCounter.count--; + } + + handler this.onConnect(connecting: SrdRef[WsConnection]) : Int as_ptr { + return 0; + } + + handler this.onReady(connecting: SrdRef[WsConnection]) : Void as_ptr {} + handler this.onData(connecting: SrdRef[WsConnection] , data : ref[String] , isBinary : Bool) : Void as_ptr {} + handler this.onClose(connecting: SrdRef[WsConnection]) : Void as_ptr {}; + + handler this.setMaxMessageSize (size : ArchInt) { + _maxMessageSize = size; + fragmentBuffer.bufferGrowSize = (_maxMessageSize / 2)~cast[ArchInt]; + } + + handler this.getMaxMessageSize() : ArchInt{ + return this._maxMessageSize; } } \ No newline at end of file From ee79a82e5a71aeb97667ddc43d788466f88a252c Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Wed, 15 Jul 2026 11:37:56 +0300 Subject: [PATCH 15/23] (websocket): instantiate user's WsRoute-derived class per endpoint --- WebPlatform/server.alusus | 141 ++++++++++------------- WebPlatform/web_socket_connection.alusus | 14 ++- 2 files changed, 71 insertions(+), 84 deletions(-) diff --git a/WebPlatform/server.alusus b/WebPlatform/server.alusus index bfdf5cd..38c48db 100644 --- a/WebPlatform/server.alusus +++ b/WebPlatform/server.alusus @@ -249,7 +249,7 @@ AstTemplateMap() .set(Srl.String("endpointMethod"), Core.Basic.TiStr(endpointParams(0))) .set(Srl.String("endpointUri"), Core.Basic.TiStr(endpointParams(1))) - .set(Srl.String("fullref"), constructElementFullReference(elements(i)~ptr)) + .set(Srl.String("fullref"), constructElementFullReference(elements(i))) ); } } @@ -301,33 +301,36 @@ function generateWebSockets(modulesRef: ref[Core.Basic.TiObject]) { def webSocketClasses : Array[ref[Core.Basic.TiObject]] = extractTypeElements(modulesRef, "wsEndpoint", "منفذ_مقبس"); + if (webSocketClasses.getLength() > 0) { // register a single set of free-function trampolines that forward each // callback to the matching bound method on the instance stored in userData, // since civetweb's C callbacks can't carry an instance pointer directly - - if (webSocketClasses.getLength() > 0) { - Spp.astMgr.insertAst( - ast { - func trampolineConnection (connection: ptr[Http.Connection], userData: ptr[Void]): Int { - def instance: ptr[WsRoute] = userData~cast[ptr[WsRoute]]; - return instance~cnt.onConnectWrapper(connection, userData); - } - func trampolineReady (connection: ptr[Http.Connection], userData: ptr[Void]): Void { - def instance: ptr[WsRoute] = userData~cast[ptr[WsRoute]]; - instance~cnt.onReadyWrapper(connection, userData); - } - func trampolineData (connection: ptr[Http.Connection], bits: Int, data: CharsPtr, dataLen: ArchWord, userData: ptr[Void]): Int { - def instance: ptr[WsRoute] = userData~cast[ptr[WsRoute]]; - return instance~cnt.onDataWrapper(connection, bits, data, dataLen, userData); - } - func trampolineClose (connection: ptr[Http.Connection], userData: ptr[Void]): Void { - def instance: ptr[WsRoute] = userData~cast[ptr[WsRoute]]; - instance~cnt.onCloseWrapper(connection, userData); - } - }, - AstTemplateMap() - ); - } + Spp.astMgr.insertAst( + ast { + func trampolineConnection (connection: ptr[Http.Connection], userData: ptr[Void]): Int { + def rc: ptr[RefCounter] = userData~cast[ptr[RefCounter]]; + def instance: SrdRef[WsRoute](rc~cnt, rc~cnt.managedObj~cast[ptr[WsRoute]]~cnt); + return instance.onConnectWrapper(connection, userData); + } + func trampolineReady (connection: ptr[Http.Connection], userData: ptr[Void]): Void { + def rc: ptr[RefCounter] = userData~cast[ptr[RefCounter]]; + def instance: SrdRef[WsRoute](rc~cnt, rc~cnt.managedObj~cast[ptr[WsRoute]]~cnt); + instance.onReadyWrapper(connection, userData); + } + func trampolineData (connection: ptr[Http.Connection], bits: Int, data: CharsPtr, dataLen: ArchWord, userData: ptr[Void]): Int { + def rc: ptr[RefCounter] = userData~cast[ptr[RefCounter]]; + def instance: SrdRef[WsRoute](rc~cnt, rc~cnt.managedObj~cast[ptr[WsRoute]]~cnt); + return instance.onDataWrapper(connection, bits, data, dataLen, userData); + } + func trampolineClose (connection: ptr[Http.Connection], userData: ptr[Void]): Void { + def rc: ptr[RefCounter] = userData~cast[ptr[RefCounter]]; + def instance: SrdRef[WsRoute](rc~cnt, rc~cnt.managedObj~cast[ptr[WsRoute]]~cnt); + instance.onCloseWrapper(connection, userData); + } + }, + AstTemplateMap() + ); + } def i : Int; for i = 0 , i < webSocketClasses.getLength(), i++ { @@ -338,22 +341,27 @@ System.fail(1 , "where is the endpoint"); } + Spp.astMgr.insertAst( ast { - def instance : webSocketClass; - instance.directTest() - Http.setWebSocketHandler( - httpContext, - "{{wsEndpointUri}}", - trampolineConnection~ptr, - trampolineReady~ptr, - trampolineData~ptr, - trampolineClose~ptr - ) + def instance: SrdRef[webSocketClass] = SrdRef[webSocketClass].construct(); + instance.wkThis.assign(instance); + + // keep this route instance alive for the server's lifetime + instance.refCounter.count++; + Http.setWebSocketHandler( + httpContext, + "{{wsEndpointUri}}", + trampolineConnection~ptr, + trampolineReady~ptr, + trampolineData~ptr, + trampolineClose~ptr, + instance.refCounter~ptr~cast[ptr[Void]] + ) }, AstTemplateMap() .set(Srl.String("wsEndpointUri"), Core.Basic.TiStr(params(0))) - .set(Srl.String("webSocketClass"), webSocketClasses(i)) + .set(Srl.String("webSocketClass"), constructElementFullReferenceByName(webSocketClasses(i))) ); } } @@ -601,20 +609,28 @@ // refer to element to an AST element using its pointer instead of referring to that element using an identifier // makes generating code dynamically easier - func constructElementFullReference (element: ptr[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { - def elementRef : ref[Core.Basic.TiObject]; - elementRef~ptr = element; + func constructElementFullReference (element: ref[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { return Core.Basic.newSrdObj[Core.Data.Ast.Passage].{ - Core.Basic.BindingOf[this].setMember("target", elementRef); + Core.Basic.BindingOf[this].setMember("target", element); }; } - // return reference for the first node child inside Block node - func firstChildOf (result: ref[SrdRef[Core.Basic.TiObject]]): ref[Core.Basic.TiObject] { - def block: SrdRef[Spp.Ast.Block] = Core.Basic.dynCastSrdRef[result, Spp.Ast.Block]; - def container: ref[Core.Basic.Containing]; - container~no_deref = Core.Basic.ContainerOf[block]; - return container.getElement(0); + func constructElementFullReferenceByName (element: ref[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { + def node: ref[Core.Data.Node](castRef[element, Core.Data.Node]); + if node.owner~ptr == 0 return SrdRef[Core.Basic.TiObject](); + def name: String = Spp.astMgr.getDefinitionName(node); + def identifier: SrdRef[Core.Data.Ast.Identifier] = Core.Basic.newSrdObj[Core.Data.Ast.Identifier].{ + value.value = name; + }; + def ownerRef: SrdRef[Core.Basic.TiObject] = constructElementFullReferenceByName(node.owner.owner); + if ownerRef.isNull() return identifier + else return Core.Basic.newSrdObj[Core.Data.Ast.LinkOperator].{ + Core.Basic.BindingOf[this].setMember("type", Core.Basic.TiStr(".")); + Core.Basic.MapContainerOf[this].{ + setElement("first", ownerRef); + setElement("second", identifier); + }; + }; } func getModifierParams (element: ref[Core.Basic.TiObject], enKwd: CharsPtr, arKwd: CharsPtr): Array[String] { @@ -703,37 +719,4 @@ } } - - - function classifyWebsocketHandlersWithEndpoint (handlersRef : Array[ref[Core.Basic.TiObject]] , webSocketEndpointsMap : ref[Map[String , Map[String , ptr[Core.Basic.TiObject]]]]) { - def handlerPossibleNames : Array[String]({ String("onConnect"), String("onReady") , String("onData") , String("onClose") }); - - def index : Int; - for index = 0 , index < handlersRef.getLength() , index++ { - // extract modifiers - def modifierParam : Array[String]; - extractModifiersParam(handlersRef(index), "wsEndpoint", "منفذ_مقبس", modifierParam); - - if (modifierParam.getLength() == 0) { - System.fail(1, "Invalid params number for Ws modifier"); - } - - // set url - def socketUrl: String = modifierParam(0); - - def socketUrlHandlers: ref[Map[String, ptr[Core.Basic.TiObject]]]; - socketUrlHandlers~ptr= webSocketEndpointsMap(socketUrl)~ptr; - - // set handler - def handlerName: String = modifierParam(1); - - def i : Int; - for i = 0 , i < handlerPossibleNames.getLength() , i++ { - if (handlerPossibleNames(i) == handlerName) { - socketUrlHandlers.set(handlerName, handlersRef(index)~ptr); - break; - } - } - } - } } diff --git a/WebPlatform/web_socket_connection.alusus b/WebPlatform/web_socket_connection.alusus index 3209fb4..5c918cd 100644 --- a/WebPlatform/web_socket_connection.alusus +++ b/WebPlatform/web_socket_connection.alusus @@ -137,12 +137,14 @@ class WsConnection { } class WsRoute { + def wkThis: WkRef[this_type]; + def _maxMessageSize : ArchInt = 1024; // fragmentation state def fragmentBuffer : StringBuilder(); def fragmentOpcode : Int; - def isFragmenting : Bool; + def isFragmenting : Bool = 0; handler this.onConnectWrapper (connection: ptr[Http.Connection], userData: ptr[Void]) : Int { def ws: SrdRef[WsConnection]; @@ -170,6 +172,8 @@ class WsRoute { def rc: ptr[RefCounter] = Http.getUserConnectionData(connection)~cast[ptr[RefCounter]]; def ws: SrdRef[WsConnection](rc~cnt, rc~cnt.managedObj~cast[ptr[WsConnection]]~cnt); + def output : Int; + def opcode : Int = bits & 0x0F; def fin: Bool = (bits & 0x80) != 0; @@ -196,7 +200,7 @@ class WsRoute { if (fin) { def isBinary : Bool = this.fragmentOpcode == 2; - this.onData(ws , this.fragmentBuffer.string ,isBinary); + output = this.onData(ws , this.fragmentBuffer.string ,isBinary); this.fragmentBuffer.clear(); } else { this.isFragmenting = 1; @@ -220,7 +224,7 @@ class WsRoute { if (fin) { def isBinary : Bool = this.fragmentOpcode == 2; - this.onData(ws , this.fragmentBuffer.string ,isBinary); + output = this.onData(ws , this.fragmentBuffer.string ,isBinary); this.fragmentBuffer.clear(); this.isFragmenting = 0; } @@ -236,7 +240,7 @@ class WsRoute { return 0; } - return 1; + return output; } handler this.onCloseWrapper (connection: ptr[Http.Connection], userData: ptr[Void]) : Void { @@ -257,7 +261,7 @@ class WsRoute { } handler this.onReady(connecting: SrdRef[WsConnection]) : Void as_ptr {} - handler this.onData(connecting: SrdRef[WsConnection] , data : ref[String] , isBinary : Bool) : Void as_ptr {} + handler this.onData(connecting: SrdRef[WsConnection] , data : ref[String] , isBinary : Bool) : Int as_ptr { return 1} handler this.onClose(connecting: SrdRef[WsConnection]) : Void as_ptr {}; handler this.setMaxMessageSize (size : ArchInt) { From 39a960af12d3a081f5fad73efecf769d673012b4 Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Wed, 15 Jul 2026 18:36:33 +0300 Subject: [PATCH 16/23] (server): enable CivetWeb's built-in ping/pong handling --- WebPlatform/server.alusus | 7 +++++++ WebPlatform/web_socket_connection.alusus | 19 ++++++------------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/WebPlatform/server.alusus b/WebPlatform/server.alusus index 38c48db..49a3277 100644 --- a/WebPlatform/server.alusus +++ b/WebPlatform/server.alusus @@ -415,6 +415,13 @@ func startServer [modulesRef: ast_ref = Root] ( mainAssetsPath: String, uiEndpointsPath: String, options: Array[CharsPtr], useSrcAssetsPath: Bool ): ptr[ServerSession] { + // Enables CivetWeb's built-in ping/pong handling for WebSocket + // connections: it auto-replies to client PING frames with PONG + // and filters PONG frames before they reach our data_handler, + // so we don't need to implement this ourselves. + options.add("enable_websocket_ping_pong"); + options.add("yes"); + // Http library requires that the last options argument is a zero, to denote // the end of options. options.add(0); diff --git a/WebPlatform/web_socket_connection.alusus b/WebPlatform/web_socket_connection.alusus index 5c918cd..b56ac73 100644 --- a/WebPlatform/web_socket_connection.alusus +++ b/WebPlatform/web_socket_connection.alusus @@ -79,11 +79,6 @@ class WsConnection { this.status = WsState.CONNECTING; } - handler this.sendText (data: CharsPtr, dataLen: ArchWord) : Int { - if this.status != WsState.OPENED return 0 ; - return Http.writeTextToWebSocket(this._connection, data, dataLen); - } - handler this.sendText (data: CharsPtr) : Int { if this.status != WsState.OPENED return 0 ; return Http.writeTextToWebSocket(this._connection, data); @@ -138,7 +133,7 @@ class WsConnection { class WsRoute { def wkThis: WkRef[this_type]; - + def _maxMessageSize : ArchInt = 1024; // fragmentation state @@ -171,9 +166,7 @@ class WsRoute { handler this.onDataWrapper (connection: ptr[Http.Connection], bits: Int, data: CharsPtr, dataLen: ArchWord, userData: ptr[Void]) : Int { def rc: ptr[RefCounter] = Http.getUserConnectionData(connection)~cast[ptr[RefCounter]]; def ws: SrdRef[WsConnection](rc~cnt, rc~cnt.managedObj~cast[ptr[WsConnection]]~cnt); - - def output : Int; - + def opcode : Int = bits & 0x0F; def fin: Bool = (bits & 0x80) != 0; @@ -200,7 +193,7 @@ class WsRoute { if (fin) { def isBinary : Bool = this.fragmentOpcode == 2; - output = this.onData(ws , this.fragmentBuffer.string ,isBinary); + this.onData(ws , this.fragmentBuffer.string ,isBinary); this.fragmentBuffer.clear(); } else { this.isFragmenting = 1; @@ -224,7 +217,7 @@ class WsRoute { if (fin) { def isBinary : Bool = this.fragmentOpcode == 2; - output = this.onData(ws , this.fragmentBuffer.string ,isBinary); + this.onData(ws , this.fragmentBuffer.string ,isBinary); this.fragmentBuffer.clear(); this.isFragmenting = 0; } @@ -240,7 +233,7 @@ class WsRoute { return 0; } - return output; + return 1; } handler this.onCloseWrapper (connection: ptr[Http.Connection], userData: ptr[Void]) : Void { @@ -261,7 +254,7 @@ class WsRoute { } handler this.onReady(connecting: SrdRef[WsConnection]) : Void as_ptr {} - handler this.onData(connecting: SrdRef[WsConnection] , data : ref[String] , isBinary : Bool) : Int as_ptr { return 1} + handler this.onData(connecting: SrdRef[WsConnection] , data : ref[String] , isBinary : Bool) as_ptr {} handler this.onClose(connecting: SrdRef[WsConnection]) : Void as_ptr {}; handler this.setMaxMessageSize (size : ArchInt) { From 68d4b3213c25b5e8d79cf80bcc21815ffbcbc0b1 Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Wed, 15 Jul 2026 18:57:54 +0300 Subject: [PATCH 17/23] refactor(server): remove duplicate function, rename extractModifiersParam, fix comment typo - Deleted a repeated/duplicate function - Renamed extractModifiersParam to extractModifierParams - Fixed a spelling mistake in a comment --- WebPlatform/server.alusus | 42 +++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/WebPlatform/server.alusus b/WebPlatform/server.alusus index 49a3277..91e6ca6 100644 --- a/WebPlatform/server.alusus +++ b/WebPlatform/server.alusus @@ -61,27 +61,33 @@ ); def i: Int; for i = 0, i < elements.getLength(), ++i { - def uriParams: Array[String] = getModifierParams(elements(i), "uiEndpoint", "منفذ_مرئي"); + def uriParams: Array[String]; + extractModifierParams(elements(i), "uiEndpoint", "منفذ_مرئي", uriParams); if uriParams.getLength() < 1 { System.fail(1, "Invalid @uiEndpoint params"); } - def titleParams: Array[String] = getModifierParams(elements(i), "title", "عنوان"); + def titleParams: Array[String]; + extractModifierParams(elements(i), "title", "عنوان", titleParams); def title: String; if titleParams.getLength() >= 1 title = titleParams(0) else title = "Alusus WebPlatform"; - def iconParams: Array[String] = getModifierParams(elements(i), "icon", "أيقونة"); + def iconParams: Array[String]; + extractModifierParams(elements(i), "icon", "أيقونة", iconParams); def icon: String; if iconParams.getLength() >= 1 icon = iconParams(0); - def appParams: Array[String] = getModifierParams(elements(i), "webApp", "تطبيق_ويب"); + def appParams: Array[String]; + extractModifierParams(elements(i), "webApp", "تطبيق_ويب" , appParams); def appManifest: String; if appParams.getLength() > 0 appManifest = appParams(0); def appVersion: String; if appParams.getLength() > 1 appVersion = appParams(1) else appVersion = "v1"; - def preCacheFilenames: Array[String] = getModifierParams(elements(i), "preCache", "خزن_مسبق"); - def dynCacheFilenames: Array[String] = getModifierParams(elements(i), "dynCache", "خزن_تفاعلي"); + def preCacheFilenames : Array[String]; + extractModifierParams(elements(i), "preCache", "خزن_مسبق", preCacheFilenames); + def dynCacheFilenames: Array[String]; + extractModifierParams(elements(i), "dynCache", "خزن_تفاعلي", dynCacheFilenames); generateUiEndpointFiles( elements(i), uriParams(0), title, icon, appManifest, appVersion, preCacheFilenames, dynCacheFilenames, @@ -215,7 +221,8 @@ ); def i: Int; for i = 0, i < elements.getLength(), ++i { - def uriParams: Array[String] = getModifierParams(elements(i), "uiEndpoint", "منفذ_مرئي"); + def uriParams: Array[String]; + extractModifierParams(elements(i), "uiEndpoint", "منفذ_مرئي" ,uriParams); if uriParams.getLength() < 1 { System.fail(1, "Invalid @uiEndpoint params"); } @@ -235,7 +242,7 @@ def i: Int; for i = 0, i < elements.getLength(), ++i { def endpointParams: Array[String]; - extractModifiersParam(elements(i), "beEndpoint", "منفذ_بياني" , endpointParams); + extractModifierParams(elements(i), "beEndpoint", "منفذ_بياني" , endpointParams); if endpointParams.getLength() < 2 { System.fail(1, "Invalid BE endpoint params"); } @@ -335,7 +342,7 @@ def i : Int; for i = 0 , i < webSocketClasses.getLength(), i++ { def params : Array[String]; - extractModifiersParam(webSocketClasses(i) , "wsEndpoint", "منفذ_مقبس" , params); + extractModifierParams(webSocketClasses(i) , "wsEndpoint", "منفذ_مقبس" , params); if (params.getLength() != 1) { System.fail(1 , "where is the endpoint"); @@ -605,16 +612,17 @@ translations ); } - function extractModifiersParam (elementRef : ref[Core.Basic.TiObject], enName : CharsPtr, arName : CharsPtr, params : ref[Array[String]]){ + + function extractModifierParams (elementRef : ref[Core.Basic.TiObject], enKwd : CharsPtr, arKwd : CharsPtr, params : ref[Array[String]]){ def translations: Map[String, String]; - translations.set(String(arName), String(enName)); + translations.set(String(arKwd), String(enKwd)); - def modifier: ref[Core.Basic.TiObject](Spp.astMgr.findModifierForElement(elementRef, enName,translations)); + def modifier: ref[Core.Basic.TiObject](Spp.astMgr.findModifierForElement(elementRef, enKwd,translations)); Spp.astMgr.getModifierStringParams(modifier, params); } - // refer to element to an AST element using its pointer instead of referring to that element using an identifier + // refer to an AST element using its pointer instead of referring to that element using an identifier // makes generating code dynamically easier func constructElementFullReference (element: ref[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { return Core.Basic.newSrdObj[Core.Data.Ast.Passage].{ @@ -640,14 +648,6 @@ }; } - func getModifierParams (element: ref[Core.Basic.TiObject], enKwd: CharsPtr, arKwd: CharsPtr): Array[String] { - def modifier: ref[Core.Basic.TiObject](Spp.astMgr.findModifierForElement(element, enKwd)); - if modifier~ptr == 0 modifier~no_deref = Spp.astMgr.findModifierForElement(element, arKwd); - def endpointParams: Array[String]; - Spp.astMgr.getModifierStringParams(modifier, endpointParams); - return endpointParams; - } - func getAllModules (astRef: ref[Core.Basic.TiObject]): Array[ref[Core.Basic.TiObject]] { def result: Array[ref[Core.Basic.TiObject]]; if astRef~ptr == Root~ast~ptr or Core.Basic.isDerivedFrom[astRef, Spp.Ast.Module] { From e87eff2fee1077a96711807cc2d4fcffcf20db36 Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Sun, 19 Jul 2026 10:19:40 +0300 Subject: [PATCH 18/23] feat(websocket): store close status code and reason for exposure via close_handler --- WebPlatform/web_socket_connection.alusus | 27 +++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/WebPlatform/web_socket_connection.alusus b/WebPlatform/web_socket_connection.alusus index b56ac73..57f1918 100644 --- a/WebPlatform/web_socket_connection.alusus +++ b/WebPlatform/web_socket_connection.alusus @@ -71,9 +71,12 @@ class WsState { class WsConnection { def _connection : ptr[Http.Connection]; - def handshakeInfo : ref[HandshakeInfo]; def status : String; + def handshakeInfo : ref[HandshakeInfo]; + def closeCode: word[16] = 1006; + def closeReason: String = String(""); + handler this~init (conn : ptr[Http.Connection]) { this._connection = conn; this.status = WsState.CONNECTING; @@ -99,6 +102,21 @@ class WsConnection { // it received). handler this.replyToClose (data : CharsPtr, dataLen : ArchWord) : Int { if this.status != WsState.OPENED return 0 ; + Console.print("this is the data len %i\n" , dataLen); + // Extract and store the close code/reason before replying, + // per RFC 6455 §5.5.1: first 2 bytes = status code, rest = reason. + + if (dataLen~cast[Int] >= 2) { + this.closeCode = ((data~cnt(0)~cast[word[16]]) << 8) | data~cnt(1)~cast[word[16]]; + if (dataLen~cast[Int] > 2) { + def reasonLen: Int = (dataLen~cast[Int] - 2); + this.closeReason = String(data + 2, reasonLen); + }} else { + // client sent a close frame with no code at all — valid per spec + this.closeCode = 1005; // "No Status Received" + this.closeReason = ""; + } + def result : Int = Http.writeToWebSocket(this._connection, 8, data, dataLen); return result; } @@ -106,6 +124,9 @@ class WsConnection { handler this.close (statusCode : word[16] , reasonMessage : CharsPtr) : Int { if this.status != WsState.OPENED return 0 ; + this.closeCode = statusCode; + this.closeReason = reasonMessage; + def reason_len : Int = String(reasonMessage).getLength(); def payload_len : Int = 2 + reason_len; @@ -241,7 +262,7 @@ class WsRoute { def ws: SrdRef[WsConnection](rc~cnt, rc~cnt.managedObj~cast[ptr[WsConnection]]~cnt); ws.status = WsState.CLOSED; - this.onClose(ws); + this.onClose(ws , ws.closeCode , ws.closeReason); // reduce counter by one cause we was increment it by one when we initialize the reference to make sure that the object // will not be released before we closing the connection @@ -255,7 +276,7 @@ class WsRoute { handler this.onReady(connecting: SrdRef[WsConnection]) : Void as_ptr {} handler this.onData(connecting: SrdRef[WsConnection] , data : ref[String] , isBinary : Bool) as_ptr {} - handler this.onClose(connecting: SrdRef[WsConnection]) : Void as_ptr {}; + handler this.onClose(connecting: SrdRef[WsConnection] , closeCode : Word[16] , closeReason : String) : Void as_ptr {}; handler this.setMaxMessageSize (size : ArchInt) { _maxMessageSize = size; From dc49adf08fff1a11164bc804d43b3ddfd8699fd7 Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Sun, 19 Jul 2026 11:18:41 +0300 Subject: [PATCH 19/23] fix(websocket): segfault in close() methode when a reason message is provided --- WebPlatform/web_socket_connection.alusus | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WebPlatform/web_socket_connection.alusus b/WebPlatform/web_socket_connection.alusus index 57f1918..3d1dad0 100644 --- a/WebPlatform/web_socket_connection.alusus +++ b/WebPlatform/web_socket_connection.alusus @@ -127,7 +127,7 @@ class WsConnection { this.closeCode = statusCode; this.closeReason = reasonMessage; - def reason_len : Int = String(reasonMessage).getLength(); + def reason_len : Int = String.getLength(reasonMessage); def payload_len : Int = 2 + reason_len; @@ -139,11 +139,11 @@ class WsConnection { // close frame payload max is 125 bytes as stated in RFC 6455 §5.5.1 def payload : array[Char, 125]; - payload(0) = (statusCode >> 8) & 0xFF; // status code, high byte + payload(0) = (statusCode >> 8)[ptr[Char]] & 0xFF; // status code, high byte payload(1) = statusCode & 0xFF; // status code, low byte if (reason_len > 0) { - Memory.copy(payload~ptr + 2, reasonMessage, reason_len); + Memory.copy(payload~ptr~cast[ptr[Char]] + 2, reasonMessage, reason_len~cast[ArchInt]); } def result : Int = Http.writeToWebSocket(this._connection, 8, payload~ptr, payload_len); From 6edc04b7967dcf6cd4d42903c1df5e886804a44e Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Sun, 19 Jul 2026 17:59:04 +0300 Subject: [PATCH 20/23] feat(websocket): handle write failures in send/close, add request/websocket timeout defaults --- WebPlatform/server.alusus | 29 ++++++++++++++++++ WebPlatform/web_socket_connection.alusus | 38 ++++++++++++++---------- 2 files changed, 52 insertions(+), 15 deletions(-) diff --git a/WebPlatform/server.alusus b/WebPlatform/server.alusus index 91e6ca6..dcb97af 100644 --- a/WebPlatform/server.alusus +++ b/WebPlatform/server.alusus @@ -422,6 +422,24 @@ func startServer [modulesRef: ast_ref = Root] ( mainAssetsPath: String, uiEndpointsPath: String, options: Array[CharsPtr], useSrcAssetsPath: Bool ): ptr[ServerSession] { + + // Bounds how long a single write (over any TCP connection this + // server handles — HTTP or WebSocket) can block on an + // unresponsive/dead peer before giving up — without this, a write can hang for a very long + // time (OS-level TCP timeout) if left unset. + if !(hasOption(options , "request_timeout_ms")) { + options.add("request_timeout_ms"); + options.add("5000"); + } + + // How long the read loop waits for incoming data before sending a PING + // falls back to request_timeout_ms if unset. Used by CivetWeb to detect and + // close unresponsive connections. + if !(hasOption(options , "websocket_timeout_ms")) { + options.add("websocket_timeout_ms"); + options.add("10000"); + } + // Enables CivetWeb's built-in ping/pong handling for WebSocket // connections: it auto-replies to client PING frames with PONG // and filters PONG frames before they reach our data_handler, @@ -589,6 +607,17 @@ // Helpers + func hasOption (options: ref[Array[CharsPtr]], key: CharsPtr): Bool { + def i: Int; + // options is a flat key/value list — check every even index (just the keys) + for i = 0, i < options.getLength(), i += 2 { + if String.isEqual(options(i), key){ + return true; + } + } + return false; + } + function extractFunctionElements (modulesRef: ref[Core.Basic.TiObject], enName: CharsPtr, arName: CharsPtr): Array[ref[Core.Basic.TiObject]] { def translations: Map[String, String]; translations.set(String(arName), String(enName)); diff --git a/WebPlatform/web_socket_connection.alusus b/WebPlatform/web_socket_connection.alusus index 3d1dad0..0d57938 100644 --- a/WebPlatform/web_socket_connection.alusus +++ b/WebPlatform/web_socket_connection.alusus @@ -82,27 +82,31 @@ class WsConnection { this.status = WsState.CONNECTING; } - handler this.sendText (data: CharsPtr) : Int { - if this.status != WsState.OPENED return 0 ; - return Http.writeTextToWebSocket(this._connection, data); + handler this.sendText (data: CharsPtr) { + if this.status != WsState.OPENED return ; + + if Http.writeTextToWebSocket(this._connection, data) <= 0 { + this.status = WsState.CLOSED; + } } - handler this.sendBinary (data: CharsPtr, dataLen: ArchWord) : Int { - if this.status != WsState.OPENED return 0 ; - return Http.writeBinaryToWebSocket(this._connection , data , dataLen); + handler this.sendBinary (data: CharsPtr, dataLen: ArchWord) { + if this.status != WsState.OPENED return ; + if Http.writeBinaryToWebSocket(this._connection , data , dataLen) <= 0 { + this.status = WsState.CLOSED; + } } - handler this.close () : Int { - return this.close(1000 , ""); + handler this.close () { + this.close(1000 , "") } // Called internally by the library to respond to a client-initiated // close frame, completing the close handshake as stated in // RFC 6455 §5.5.1 (a responder typically echoes the status code // it received). - handler this.replyToClose (data : CharsPtr, dataLen : ArchWord) : Int { - if this.status != WsState.OPENED return 0 ; - Console.print("this is the data len %i\n" , dataLen); + handler this.replyToClose (data : CharsPtr, dataLen : ArchWord) { + if this.status != WsState.OPENED return; // Extract and store the close code/reason before replying, // per RFC 6455 §5.5.1: first 2 bytes = status code, rest = reason. @@ -117,12 +121,14 @@ class WsConnection { this.closeReason = ""; } - def result : Int = Http.writeToWebSocket(this._connection, 8, data, dataLen); + if Http.writeToWebSocket(this._connection, 8, data, dataLen) <= 0 { + this.status = WsState.CLOSED; + } return result; } - handler this.close (statusCode : word[16] , reasonMessage : CharsPtr) : Int { - if this.status != WsState.OPENED return 0 ; + handler this.close (statusCode : word[16] , reasonMessage : CharsPtr) { + if this.status != WsState.OPENED return; this.closeCode = statusCode; this.closeReason = reasonMessage; @@ -146,7 +152,9 @@ class WsConnection { Memory.copy(payload~ptr~cast[ptr[Char]] + 2, reasonMessage, reason_len~cast[ArchInt]); } - def result : Int = Http.writeToWebSocket(this._connection, 8, payload~ptr, payload_len); + if Http.writeToWebSocket(this._connection, 8, payload~ptr, payload_len) <= { + this.status = WsState.CLOSED; + } return result; } From d9b2c14f90da4470d29eeca94851a2c23a564dea Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Sun, 19 Jul 2026 18:10:04 +0300 Subject: [PATCH 21/23] feat(websocket): add getStatus() and CLOSING transition to close paths --- WebPlatform/web_socket_connection.alusus | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/WebPlatform/web_socket_connection.alusus b/WebPlatform/web_socket_connection.alusus index 0d57938..44e0000 100644 --- a/WebPlatform/web_socket_connection.alusus +++ b/WebPlatform/web_socket_connection.alusus @@ -82,6 +82,10 @@ class WsConnection { this.status = WsState.CONNECTING; } + handler this.getStatus(): String { + return this.status; + } + handler this.sendText (data: CharsPtr) { if this.status != WsState.OPENED return ; @@ -107,6 +111,7 @@ class WsConnection { // it received). handler this.replyToClose (data : CharsPtr, dataLen : ArchWord) { if this.status != WsState.OPENED return; + // Extract and store the close code/reason before replying, // per RFC 6455 §5.5.1: first 2 bytes = status code, rest = reason. @@ -120,10 +125,13 @@ class WsConnection { this.closeCode = 1005; // "No Status Received" this.closeReason = ""; } + + this.status = WsState.CLOSING; if Http.writeToWebSocket(this._connection, 8, data, dataLen) <= 0 { this.status = WsState.CLOSED; } + return result; } @@ -152,6 +160,8 @@ class WsConnection { Memory.copy(payload~ptr~cast[ptr[Char]] + 2, reasonMessage, reason_len~cast[ArchInt]); } + this.status = WsState.CLOSING; + if Http.writeToWebSocket(this._connection, 8, payload~ptr, payload_len) <= { this.status = WsState.CLOSED; } From ae4d7942a807cce05f29a945b0f05b64911131d4 Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Sun, 19 Jul 2026 18:35:20 +0300 Subject: [PATCH 22/23] fix(websocket): reject unrecognized/reserved opcodes as a protocol error --- WebPlatform/web_socket_connection.alusus | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/WebPlatform/web_socket_connection.alusus b/WebPlatform/web_socket_connection.alusus index 44e0000..1b9e278 100644 --- a/WebPlatform/web_socket_connection.alusus +++ b/WebPlatform/web_socket_connection.alusus @@ -272,6 +272,11 @@ class WsRoute { return 0; } + if (opcode != 0 and opcode != 1 and opcode != 2 and opcode != 8) { + ws.close(1002, "unsupported opcode"); + return 0; + } + return 1; } From c239f2a8639b3ee1fe5dd25a35b74e120f8c3129 Mon Sep 17 00:00:00 2001 From: Sarmad Khalid Abdullah Date: Wed, 29 Jul 2026 18:16:41 -0700 Subject: [PATCH 23/23] Clean up the design of web sockets --- WebPlatform.alusus | 2 + WebPlatform/WsConnection.alusus | 212 +++++++++++++++ WebPlatform/server.alusus | 194 +++++++------- WebPlatform/web_socket_connection.alusus | 312 ----------------------- 4 files changed, 302 insertions(+), 418 deletions(-) create mode 100644 WebPlatform/WsConnection.alusus delete mode 100644 WebPlatform/web_socket_connection.alusus diff --git a/WebPlatform.alusus b/WebPlatform.alusus index 2bf0128..1807211 100644 --- a/WebPlatform.alusus +++ b/WebPlatform.alusus @@ -31,12 +31,14 @@ import "Spp/Ast"; import "Build"; import "Apm"; import "closure"; +Apm.importPackage("Alusus/Sle@0.2", "Srl/enums.alusus"); Apm.importPackage("Alusus/Http@0.3"); Apm.importPackage("Alusus/Json@0.2"); Apm.importPackage("Alusus/MarkdownTranslator@0.1"); Apm.importPackage("Alusus/Promises@0.1"); import "WebPlatform/server"; +import "WebPlatform/WsConnection"; import "WebPlatform/browser_api"; import "WebPlatform/frontend_helpers"; import "WebPlatform/Styling/Color"; diff --git a/WebPlatform/WsConnection.alusus b/WebPlatform/WsConnection.alusus new file mode 100644 index 0000000..141d3dd --- /dev/null +++ b/WebPlatform/WsConnection.alusus @@ -0,0 +1,212 @@ +@merge module WebPlatform { + class WsStatus { + setupStringEnum[]; + enumStringValue[CONNECTING, "connecting"]; + enumStringValue[OPENED, "opened"]; + enumStringValue[CLOSING, "closing"]; + enumStringValue[CLOSED, "closed"]; + } + + class WsConnection { + def wkThis: WkRef[this_type]; + def connection: ptr[Http.Connection]; + + def status: WsStatus; + def closeCode: word[16] = 1006; + def closeReason: String; + + def maxMessageSize: ArchInt = 1024; + def fragmentBuffer: StringBuilder(); + def fragmentOpcode: Int; + def isFragmenting: Bool = 0; + + handler this~init(conn: ptr[Http.Connection]) { + this.connection = conn; + this.status = WsStatus.CONNECTING; + } + + handler this.getStatus(): WsStatus { + return this.status; + } + + handler this.setMaxMessageSize(size: ArchInt) { + this.maxMessageSize = size; + fragmentBuffer.bufferGrowSize = (maxMessageSize / 2)~cast[ArchInt]; + } + + handler this.getMaxMessageSize(): ArchInt{ + return this.maxMessageSize; + } + + handler this.sendText(data: CharsPtr) { + if this.status != WsStatus.OPENED return ; + + if Http.writeTextToWebSocket(this.connection, data) <= 0 { + this.status = WsStatus.CLOSED; + } + } + + handler this.sendBinary(data: CharsPtr, dataLen: ArchWord) { + if this.status != WsStatus.OPENED return ; + if Http.writeBinaryToWebSocket(this.connection , data , dataLen) <= 0 { + this.status = WsStatus.CLOSED; + } + } + + handler this.close () { + this.close(1000 , "") + } + + handler this.close (statusCode : word[16] , reasonMessage : CharsPtr) { + if this.status != WsStatus.OPENED return; + + this.closeCode = statusCode; + this.closeReason = reasonMessage; + + def reasonLen : Int = String.getLength(reasonMessage); + + def payloadLen : Int = 2 + reasonLen; + + if (payloadLen > 125) { + reasonLen = 123; // 125 - 2 bytes for the status code + payloadLen = 125; + } + + // close frame payload max is 125 bytes as stated in RFC 6455 §5.5.1 + def payload : array[Char, 125]; + + payload(0) = (statusCode >> 8)[ptr[Char]] & 0xFF; // status code, high byte + payload(1) = statusCode & 0xFF; // status code, low byte + + if (reasonLen > 0) { + Memory.copy(payload~ptr~cast[ptr[Char]] + 2, reasonMessage, reasonLen~cast[ArchInt]); + } + + this.status = WsStatus.CLOSING; + + if Http.writeToWebSocket(this.connection, 8, payload~ptr, payloadLen) <= { + this.status = WsStatus.CLOSED; + } + + return result; + } + + // Called internally by the library to respond to a client-initiated + // close frame, completing the close handshake as stated in + // RFC 6455 §5.5.1 (a responder typically echoes the status code + // it received). + handler this.replyToClose (data : CharsPtr, dataLen : ArchWord) { + if this.status != WsStatus.OPENED return; + + // Extract and store the close code/reason before replying, + // per RFC 6455 §5.5.1: first 2 bytes = status code, rest = reason. + + if (dataLen~cast[Int] >= 2) { + this.closeCode = ((data~cnt(0)~cast[word[16]]) << 8) | data~cnt(1)~cast[word[16]]; + if (dataLen~cast[Int] > 2) { + def reasonLen: Int = (dataLen~cast[Int] - 2); + this.closeReason = String(data + 2, reasonLen); + } + } else { + // client sent a close frame with no code at all — valid per spec + this.closeCode = 1005; // "No Status Received" + this.closeReason = ""; + } + + this.status = WsStatus.CLOSING; + + if Http.writeToWebSocket(this.connection, 8, data, dataLen) <= 0 { + this.status = WsStatus.CLOSED; + } + + return result; + } + + handler this.onConnect(): Int as_ptr { + return 0; + } + + handler this.onReady() as_ptr { + } + + handler this.onData(bits: Int, data: CharsPtr, dataLen: ArchWord): Int { + def opcode : Int = bits & 0x0F; + def fin: Bool = (bits & 0x80) != 0; + + // Safe to cast: CivetWeb rejects any frame exceeding ~2 GiB (0x7FFF0000), + // which is within ArchInt's range, so this cast can't overflow or go negative. + + def dataLen : ArchInt = dataLen~cast[ArchInt]; + + if opcode == 1 or opcode == 2 { + if (this.isFragmenting) { + this.close(1002, "unexpected new message mid-fragment"); + return 0; + } + + this.fragmentOpcode = opcode; + this.fragmentBuffer.clear(); + + if (dataLen > this.getMaxMessageSize()) { + this.close(1009, "Message too big"); + return 0; + } + + this.fragmentBuffer.append(data, dataLen); + + if (fin) { + def isBinary : Bool = this.fragmentOpcode == 2; + this.onUserData(this.fragmentBuffer.string, isBinary); + this.fragmentBuffer.clear(); + } else { + this.isFragmenting = 1; + } + } + + if opcode == 0 { + if (!this.isFragmenting) { + // Protocol violation: CONTINUATION with no preceding TEXT/BINARY + this.close(1002, "unexpected continuation frame"); + return 0; + } + + if (this.fragmentBuffer.getLength() + dataLen > this.getMaxMessageSize()) { + this.close(1009, "Message too big"); + return 0; + } + + this.fragmentBuffer.append(data, dataLen); + + if (fin) { + def isBinary : Bool = this.fragmentOpcode == 2; + this.onUserData(this.fragmentBuffer.string, isBinary); + this.fragmentBuffer.clear(); + this.isFragmenting = 0; + } + } + + if opcode == 8 { + // reply with a close frame before tearing down — completes the handshake properly + if dataLen~cast[Int] >= 2 { + this.replyToClose(data, dataLen); // echo back client's code+reason + } else { + this.close(1000, ""); // client sent no code, reply with normal closure + } + return 0; + } + + if (opcode != 0 and opcode != 1 and opcode != 2 and opcode != 8) { + this.close(1002, "unsupported opcode"); + return 0; + } + + return 1; + } + + handler this.onUserData(data: ref[String], isBinary: Bool) as_ptr { + } + + handler this.onClose() as_ptr { + } + } +} diff --git a/WebPlatform/server.alusus b/WebPlatform/server.alusus index dcb97af..85307ea 100644 --- a/WebPlatform/server.alusus +++ b/WebPlatform/server.alusus @@ -61,33 +61,28 @@ ); def i: Int; for i = 0, i < elements.getLength(), ++i { - def uriParams: Array[String]; - extractModifierParams(elements(i), "uiEndpoint", "منفذ_مرئي", uriParams); + def uriParams: Array[String] = getModifierParams(elements(i), "uiEndpoint", "منفذ_مرئي"); if uriParams.getLength() < 1 { + // TODO: Raise build notice instead System.fail(1, "Invalid @uiEndpoint params"); } - def titleParams: Array[String]; - extractModifierParams(elements(i), "title", "عنوان", titleParams); + def titleParams: Array[String] = getModifierParams(elements(i), "title", "عنوان"); def title: String; if titleParams.getLength() >= 1 title = titleParams(0) else title = "Alusus WebPlatform"; - def iconParams: Array[String]; - extractModifierParams(elements(i), "icon", "أيقونة", iconParams); + def iconParams: Array[String] = getModifierParams(elements(i), "icon", "أيقونة"); def icon: String; if iconParams.getLength() >= 1 icon = iconParams(0); - def appParams: Array[String]; - extractModifierParams(elements(i), "webApp", "تطبيق_ويب" , appParams); + def appParams: Array[String] = getModifierParams(elements(i), "webApp", "تطبيق_ويب"); def appManifest: String; if appParams.getLength() > 0 appManifest = appParams(0); def appVersion: String; if appParams.getLength() > 1 appVersion = appParams(1) else appVersion = "v1"; - def preCacheFilenames : Array[String]; - extractModifierParams(elements(i), "preCache", "خزن_مسبق", preCacheFilenames); - def dynCacheFilenames: Array[String]; - extractModifierParams(elements(i), "dynCache", "خزن_تفاعلي", dynCacheFilenames); + def preCacheFilenames: Array[String] = getModifierParams(elements(i), "preCache", "خزن_مسبق"); + def dynCacheFilenames: Array[String] = getModifierParams(elements(i), "dynCache", "خزن_تفاعلي"); generateUiEndpointFiles( elements(i), uriParams(0), title, icon, appManifest, appVersion, preCacheFilenames, dynCacheFilenames, @@ -221,9 +216,9 @@ ); def i: Int; for i = 0, i < elements.getLength(), ++i { - def uriParams: Array[String]; - extractModifierParams(elements(i), "uiEndpoint", "منفذ_مرئي" ,uriParams); + def uriParams: Array[String] = getModifierParams(elements(i), "uiEndpoint", "منفذ_مرئي"); if uriParams.getLength() < 1 { + // TODO: Raise build notice instead System.fail(1, "Invalid @uiEndpoint params"); } def fnName: String = Spp.astMgr.getDefinitionName(elements(i)); @@ -238,12 +233,12 @@ } func generateBeEndpointsCalls (parent: ref[Core.Basic.TiObject]) { - def elements : Array[ref[Core.Basic.TiObject]] = extractFunctionElements(parent, "beEndpoint", "منفذ_بياني"); + def elements : Array[ref[Core.Basic.TiObject]] = findFunctionElements(parent, "beEndpoint", "منفذ_بياني"); def i: Int; for i = 0, i < elements.getLength(), ++i { - def endpointParams: Array[String]; - extractModifierParams(elements(i), "beEndpoint", "منفذ_بياني" , endpointParams); + def endpointParams: Array[String] = getModifierParams(elements(i), "beEndpoint", "منفذ_بياني"); if endpointParams.getLength() < 2 { + // TODO: Raise build notice instead System.fail(1, "Invalid BE endpoint params"); } Spp.astMgr.insertAst( @@ -276,6 +271,7 @@ ); } } + func generateWebSocketEndpointChecks (parent: ref[Core.Basic.TiObject]) { def classElements: Array[ref[Core.Basic.TiObject]] = Spp.astMgr.findElements( ast { modifier == "wsEndpoint" || modifier == "منفذ_مقبس" }, @@ -290,7 +286,8 @@ def wsEndpointParam: Array[String]; if !Spp.astMgr.getModifierStringParams(modifier, wsEndpointParam) - || wsEndpointParam.getLength() != 1 { + or wsEndpointParam.getLength() != 1 { + // TODO: Raise build notice instead. System.fail(1, "Invalid WS endpoint params"); } Spp.astMgr.insertAst( @@ -305,73 +302,37 @@ } } - function generateWebSockets(modulesRef: ref[Core.Basic.TiObject]) { - def webSocketClasses : Array[ref[Core.Basic.TiObject]] = extractTypeElements(modulesRef, "wsEndpoint", "منفذ_مقبس"); - - if (webSocketClasses.getLength() > 0) { - // register a single set of free-function trampolines that forward each - // callback to the matching bound method on the instance stored in userData, - // since civetweb's C callbacks can't carry an instance pointer directly - Spp.astMgr.insertAst( - ast { - func trampolineConnection (connection: ptr[Http.Connection], userData: ptr[Void]): Int { - def rc: ptr[RefCounter] = userData~cast[ptr[RefCounter]]; - def instance: SrdRef[WsRoute](rc~cnt, rc~cnt.managedObj~cast[ptr[WsRoute]]~cnt); - return instance.onConnectWrapper(connection, userData); - } - func trampolineReady (connection: ptr[Http.Connection], userData: ptr[Void]): Void { - def rc: ptr[RefCounter] = userData~cast[ptr[RefCounter]]; - def instance: SrdRef[WsRoute](rc~cnt, rc~cnt.managedObj~cast[ptr[WsRoute]]~cnt); - instance.onReadyWrapper(connection, userData); - } - func trampolineData (connection: ptr[Http.Connection], bits: Int, data: CharsPtr, dataLen: ArchWord, userData: ptr[Void]): Int { - def rc: ptr[RefCounter] = userData~cast[ptr[RefCounter]]; - def instance: SrdRef[WsRoute](rc~cnt, rc~cnt.managedObj~cast[ptr[WsRoute]]~cnt); - return instance.onDataWrapper(connection, bits, data, dataLen, userData); - } - func trampolineClose (connection: ptr[Http.Connection], userData: ptr[Void]): Void { - def rc: ptr[RefCounter] = userData~cast[ptr[RefCounter]]; - def instance: SrdRef[WsRoute](rc~cnt, rc~cnt.managedObj~cast[ptr[WsRoute]]~cnt); - instance.onCloseWrapper(connection, userData); - } - }, - AstTemplateMap() - ); - } + function generateWebSocketRegistrations (modulesRef: ref[Core.Basic.TiObject]) { + def webSocketClasses: Array[ref[Core.Basic.TiObject]] = + findTypeElements(modulesRef, "wsEndpoint", "منفذ_مقبس"); def i : Int; - for i = 0 , i < webSocketClasses.getLength(), i++ { - def params : Array[String]; - extractModifierParams(webSocketClasses(i) , "wsEndpoint", "منفذ_مقبس" , params); + for i = 0, i < webSocketClasses.getLength(), i++ { + def params : Array[String] = getModifierParams(webSocketClasses(i) , "wsEndpoint", "منفذ_مقبس"); if (params.getLength() != 1) { System.fail(1 , "where is the endpoint"); } - Spp.astMgr.insertAst( ast { - def instance: SrdRef[webSocketClass] = SrdRef[webSocketClass].construct(); - instance.wkThis.assign(instance); - - // keep this route instance alive for the server's lifetime - instance.refCounter.count++; Http.setWebSocketHandler( httpContext, "{{wsEndpointUri}}", - trampolineConnection~ptr, - trampolineReady~ptr, - trampolineData~ptr, - trampolineClose~ptr, - instance.refCounter~ptr~cast[ptr[Void]] + wsConnectCallback[webSocketClass]~ptr, + wsReadyCallback[webSocketClass]~ptr, + wsDataCallback[webSocketClass]~ptr, + wsCloseCallback[webSocketClass]~ptr, + null ) }, AstTemplateMap() .set(Srl.String("wsEndpointUri"), Core.Basic.TiStr(params(0))) - .set(Srl.String("webSocketClass"), constructElementFullReferenceByName(webSocketClasses(i))) + .set(Srl.String("webSocketClass"), constructElementFullReference(webSocketClasses(i))) ); } } + // Querying Functions func getAssetsRoutesFromModules (modulesRef: ref[Core.Basic.TiObject]): Array[StaticRoute] { @@ -422,7 +383,6 @@ func startServer [modulesRef: ast_ref = Root] ( mainAssetsPath: String, uiEndpointsPath: String, options: Array[CharsPtr], useSrcAssetsPath: Bool ): ptr[ServerSession] { - // Bounds how long a single write (over any TCP connection this // server handles — HTTP or WebSocket) can block on an // unresponsive/dead peer before giving up — without this, a write can hang for a very long @@ -478,7 +438,7 @@ Spp.buildMgr.raiseBuildNotice("SPPA1002", 1, modulesRef~ast); } def i: Int; - for i = 0, i < modules.getLength(), ++i generateWebSockets(modules(i)); + for i = 0, i < modules.getLength(), ++i generateWebSocketRegistrations(modules(i)); }; return session; @@ -596,6 +556,45 @@ return 1; } + func wsConnectCallback [WsConnClass: type] (connection: ptr[Http.Connection], userData: ptr[Void]): Int { + def ws: SrdRef[WsConnClass]; + ws.alloc()~init(connection); + ws.wkThis = ws; + + // we increment counter by one so we don't lose the object even if the user + // does not reference it + ws.refCounter.count++; + Http.setUserConnectionData(connection, ws.refCounter~ptr; + + return ws.onConnect(); + } + + func wsReadyCallback [WsConnClass: type] (connection: ptr[Http.Connection], userData: ptr[Void]): Void { + def rc: ptr[RefCounter] = Http.getUserConnectionData(connection)~cast[ptr[RefCounter]]; + def ws: SrdRef[WsConnClass](rc~cnt, castRef[rc~cnt.managedObj, WsConnClass]); + ws.status = WsStatus.OPENED; + ws.onReady(); + } + + func wsDataCallback [WsConnClass: type] ( + connection: ptr[Http.Connection], bits: Int, data: CharsPtr, dataLen: ArchWord, userData: ptr[Void] + ): Int { + def rc: ptr[RefCounter] = Http.getUserConnectionData(connection)~cast[ptr[RefCounter]]; + def ws: SrdRef[WsConnClass](rc~cnt, castRef[rc~cnt.managedObj, WsConnClass]); + return ws.onData(bits, data, dataLen); + } + + func wsCloseCallback [WsConnClass: type] (connection: ptr[Http.Connection], userData: ptr[Void]): Void { + def rc: ptr[RefCounter] = Http.getUserConnectionData(connection)~cast[ptr[RefCounter]]; + def ws: SrdRef[WsConnClass](rc~cnt, castRef[rc~cnt.managedObj, WsConnClass]); + ws.status = WsStatus.CLOSED; + ws.onClose(); + + // We no longer need this connection object, so decrease the reference counter which was manually + // incremented before being set on the Http.Connection instance. + ws.refCounter.count--; + } + func return404(connection: ptr[Http.Connection], uri: CharsPtr) { def content: array[Char, 1024]; String.assign(content~ptr, "

404 - Not Found

you are in \"%.512s\"", uri); @@ -618,7 +617,24 @@ return false; } - function extractFunctionElements (modulesRef: ref[Core.Basic.TiObject], enName: CharsPtr, arName: CharsPtr): Array[ref[Core.Basic.TiObject]] { + func constructElementFullReference (element: ref[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { + return Core.Basic.newSrdObj[Core.Data.Ast.Passage].{ + Core.Basic.BindingOf[this].setMember("target", element); + }; + } + + func getModifierParams (element: ref[Core.Basic.TiObject], enKwd: CharsPtr, arKwd: CharsPtr): Array[String] { + def translations: Map[String, String]; + translations.set(String(arKwd), String(enKwd)); + def modifier: ref[Core.Basic.TiObject](Spp.astMgr.findModifierForElement(elementRef, enKwd, translations)); + def endpointParams: Array[String]; + Spp.astMgr.getModifierStringParams(modifier, endpointParams); + return endpointParams; + } + + function findFunctionElements ( + modulesRef: ref[Core.Basic.TiObject], enName: CharsPtr, arName: CharsPtr + ): Array[ref[Core.Basic.TiObject]] { def translations: Map[String, String]; translations.set(String(arName), String(enName)); return Spp.astMgr.findElements( @@ -630,7 +646,9 @@ ); } - function extractTypeElements (modulesRef: ref[Core.Basic.TiObject], enName: CharsPtr, arName: CharsPtr): Array[ref[Core.Basic.TiObject]] { + function findTypeElements ( + modulesRef: ref[Core.Basic.TiObject], enName: CharsPtr, arName: CharsPtr + ): Array[ref[Core.Basic.TiObject]] { def translations: Map[String, String]; translations.set(String(arName), String(enName)); return Spp.astMgr.findElements( @@ -642,41 +660,6 @@ ); } - function extractModifierParams (elementRef : ref[Core.Basic.TiObject], enKwd : CharsPtr, arKwd : CharsPtr, params : ref[Array[String]]){ - - def translations: Map[String, String]; - translations.set(String(arKwd), String(enKwd)); - - def modifier: ref[Core.Basic.TiObject](Spp.astMgr.findModifierForElement(elementRef, enKwd,translations)); - Spp.astMgr.getModifierStringParams(modifier, params); - } - - // refer to an AST element using its pointer instead of referring to that element using an identifier - // makes generating code dynamically easier - func constructElementFullReference (element: ref[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { - return Core.Basic.newSrdObj[Core.Data.Ast.Passage].{ - Core.Basic.BindingOf[this].setMember("target", element); - }; - } - - func constructElementFullReferenceByName (element: ref[Core.Basic.TiObject]): SrdRef[Core.Basic.TiObject] { - def node: ref[Core.Data.Node](castRef[element, Core.Data.Node]); - if node.owner~ptr == 0 return SrdRef[Core.Basic.TiObject](); - def name: String = Spp.astMgr.getDefinitionName(node); - def identifier: SrdRef[Core.Data.Ast.Identifier] = Core.Basic.newSrdObj[Core.Data.Ast.Identifier].{ - value.value = name; - }; - def ownerRef: SrdRef[Core.Basic.TiObject] = constructElementFullReferenceByName(node.owner.owner); - if ownerRef.isNull() return identifier - else return Core.Basic.newSrdObj[Core.Data.Ast.LinkOperator].{ - Core.Basic.BindingOf[this].setMember("type", Core.Basic.TiStr(".")); - Core.Basic.MapContainerOf[this].{ - setElement("first", ownerRef); - setElement("second", identifier); - }; - }; - } - func getAllModules (astRef: ref[Core.Basic.TiObject]): Array[ref[Core.Basic.TiObject]] { def result: Array[ref[Core.Basic.TiObject]]; if astRef~ptr == Root~ast~ptr or Core.Basic.isDerivedFrom[astRef, Spp.Ast.Module] { @@ -754,5 +737,4 @@ System.fail(1, String("Invalid asset route; path should end with /: ") + buildPath); } } - } diff --git a/WebPlatform/web_socket_connection.alusus b/WebPlatform/web_socket_connection.alusus deleted file mode 100644 index 1b9e278..0000000 --- a/WebPlatform/web_socket_connection.alusus +++ /dev/null @@ -1,312 +0,0 @@ -function copyHandshake(info: ptr[Http.RequestInfo]): ref[HandshakeInfo] { - def result: ref[HandshakeInfo]; - result~ptr = Memory.alloc(HandshakeInfo~size)~cast[ptr[HandshakeInfo]]; - - result.requestMethod = String(info~cnt.requestMethod); - - result.requestUri = String(info~cnt.requestUri); - result.localUri = String(info~cnt.localUri); - if (info~cnt.queryString != null) - result.queryString = String(info~cnt.queryString) - else - result.queryString = String(""); - - result.remoteAddr = String(info~cnt.remoteAddr~ptr); - - result.remotePort = info~cnt.remotePort; - result.isSsl = info~cnt.isSsl; - - result.numberHeaders = info~cnt.numberHeaders; - result.headers.reserve(info~cnt.numberHeaders); - - def i : Int; - def h: HandshakeHeader; - for i = 0 , i < info~cnt.numberHeaders , i++ { - h.name = String(info~cnt.httpHeaders(i).name); - h.value = String(info~cnt.httpHeaders(i).value); - result.headers.add(h); - } - - return result; -} - -class HandshakeHeader { - def name: String; - def value: String; - handler this~init(){}; - handler this~init(src: ref[HandshakeHeader]) { - this.name = src.name; - this.value = src.value; - }; -}; - -class HandshakeInfo { - def requestMethod: String; - def requestUri: String; - def localUri: String; - def queryString: String; - def remoteAddr: String; - def remotePort: Int; - def isSsl: Int; - def headers: Array[HandshakeHeader]; - def numberHeaders: Int; - - handler this.getHeader(name: String): String { - def i : Int; - for i = 0 , i < this.headers.getLength() , i++{ - if (this.headers(i).name == name) { - return this.headers(i).value; - } - } - return String(""); - } -}; - -class WsState { - @shared def CONNECTING : String("connecting"); - @shared def OPENED : String("opened"); - @shared def CLOSING : String("closing"); - @shared def CLOSED : String("closed"); -} - -class WsConnection { - def _connection : ptr[Http.Connection]; - def status : String; - - def handshakeInfo : ref[HandshakeInfo]; - def closeCode: word[16] = 1006; - def closeReason: String = String(""); - - handler this~init (conn : ptr[Http.Connection]) { - this._connection = conn; - this.status = WsState.CONNECTING; - } - - handler this.getStatus(): String { - return this.status; - } - - handler this.sendText (data: CharsPtr) { - if this.status != WsState.OPENED return ; - - if Http.writeTextToWebSocket(this._connection, data) <= 0 { - this.status = WsState.CLOSED; - } - } - - handler this.sendBinary (data: CharsPtr, dataLen: ArchWord) { - if this.status != WsState.OPENED return ; - if Http.writeBinaryToWebSocket(this._connection , data , dataLen) <= 0 { - this.status = WsState.CLOSED; - } - } - - handler this.close () { - this.close(1000 , "") - } - - // Called internally by the library to respond to a client-initiated - // close frame, completing the close handshake as stated in - // RFC 6455 §5.5.1 (a responder typically echoes the status code - // it received). - handler this.replyToClose (data : CharsPtr, dataLen : ArchWord) { - if this.status != WsState.OPENED return; - - // Extract and store the close code/reason before replying, - // per RFC 6455 §5.5.1: first 2 bytes = status code, rest = reason. - - if (dataLen~cast[Int] >= 2) { - this.closeCode = ((data~cnt(0)~cast[word[16]]) << 8) | data~cnt(1)~cast[word[16]]; - if (dataLen~cast[Int] > 2) { - def reasonLen: Int = (dataLen~cast[Int] - 2); - this.closeReason = String(data + 2, reasonLen); - }} else { - // client sent a close frame with no code at all — valid per spec - this.closeCode = 1005; // "No Status Received" - this.closeReason = ""; - } - - this.status = WsState.CLOSING; - - if Http.writeToWebSocket(this._connection, 8, data, dataLen) <= 0 { - this.status = WsState.CLOSED; - } - - return result; - } - - handler this.close (statusCode : word[16] , reasonMessage : CharsPtr) { - if this.status != WsState.OPENED return; - - this.closeCode = statusCode; - this.closeReason = reasonMessage; - - def reason_len : Int = String.getLength(reasonMessage); - - def payload_len : Int = 2 + reason_len; - - if (payload_len > 125) { - reason_len = 123; // 125 - 2 bytes for the status code - payload_len = 125; - } - - // close frame payload max is 125 bytes as stated in RFC 6455 §5.5.1 - def payload : array[Char, 125]; - - payload(0) = (statusCode >> 8)[ptr[Char]] & 0xFF; // status code, high byte - payload(1) = statusCode & 0xFF; // status code, low byte - - if (reason_len > 0) { - Memory.copy(payload~ptr~cast[ptr[Char]] + 2, reasonMessage, reason_len~cast[ArchInt]); - } - - this.status = WsState.CLOSING; - - if Http.writeToWebSocket(this._connection, 8, payload~ptr, payload_len) <= { - this.status = WsState.CLOSED; - } - - return result; - } -} - -class WsRoute { - def wkThis: WkRef[this_type]; - - def _maxMessageSize : ArchInt = 1024; - - // fragmentation state - def fragmentBuffer : StringBuilder(); - def fragmentOpcode : Int; - def isFragmenting : Bool = 0; - - handler this.onConnectWrapper (connection: ptr[Http.Connection], userData: ptr[Void]) : Int { - def ws: SrdRef[WsConnection]; - ws.alloc(); - ws.obj~init(connection); - - // we increment counter by one so we don't lose the object even if the user - // does not reference it - - ws.refCounter.count++; - Http.setUserConnectionData(connection, ws.refCounter~ptr~cast[ptr[RefCounter]]); - - return this.onConnect(ws); - } - handler this.onReadyWrapper (connection: ptr[Http.Connection], userData: ptr[Void]) : Void { - def rc: ptr[RefCounter] = Http.getUserConnectionData(connection)~cast[ptr[RefCounter]]; - def ws: SrdRef[WsConnection](rc~cnt, rc~cnt.managedObj~cast[ptr[WsConnection]]~cnt); - - ws.status = WsState.OPENED; - - this.onReady(ws); - } - - handler this.onDataWrapper (connection: ptr[Http.Connection], bits: Int, data: CharsPtr, dataLen: ArchWord, userData: ptr[Void]) : Int { - def rc: ptr[RefCounter] = Http.getUserConnectionData(connection)~cast[ptr[RefCounter]]; - def ws: SrdRef[WsConnection](rc~cnt, rc~cnt.managedObj~cast[ptr[WsConnection]]~cnt); - - def opcode : Int = bits & 0x0F; - def fin: Bool = (bits & 0x80) != 0; - - // Safe to cast: CivetWeb rejects any frame exceeding ~2 GiB (0x7FFF0000), - // which is within ArchInt's range, so this cast can't overflow or go negative. - - def data_Len : ArchInt = dataLen~cast[ArchInt]; - - if opcode == 1 or opcode == 2 { - if (this.isFragmenting) { - ws.close(1002, "unexpected new message mid-fragment"); - return 0; - } - - this.fragmentOpcode = opcode; - this.fragmentBuffer.clear(); - - if (data_Len > this.getMaxMessageSize()) { - ws.close(1009, "Message too big"); - return 0; - } - - this.fragmentBuffer.append(data, data_Len); - - if (fin) { - def isBinary : Bool = this.fragmentOpcode == 2; - this.onData(ws , this.fragmentBuffer.string ,isBinary); - this.fragmentBuffer.clear(); - } else { - this.isFragmenting = 1; - } - } - - if opcode == 0 { - - if (!this.isFragmenting) { - // Protocol violation: CONTINUATION with no preceding TEXT/BINARY - ws.close(1002, "unexpected continuation frame"); - return 0; - } - - if (this.fragmentBuffer.getLength() + data_Len > this.getMaxMessageSize()) { - ws.close(1009, "Message too big"); - return 0; - } - - this.fragmentBuffer.append(data, data_Len); - - if (fin) { - def isBinary : Bool = this.fragmentOpcode == 2; - this.onData(ws , this.fragmentBuffer.string ,isBinary); - this.fragmentBuffer.clear(); - this.isFragmenting = 0; - } - } - - if opcode == 8 { - // reply with a close frame before tearing down — completes the handshake properly - if data_Len~cast[Int] >= 2 { - ws.replyToClose(data, dataLen); // echo back client's code+reason - } else { - ws.close(1000, ""); // client sent no code, reply with normal closure - } - return 0; - } - - if (opcode != 0 and opcode != 1 and opcode != 2 and opcode != 8) { - ws.close(1002, "unsupported opcode"); - return 0; - } - - return 1; - } - - handler this.onCloseWrapper (connection: ptr[Http.Connection], userData: ptr[Void]) : Void { - def rc: ptr[RefCounter] = Http.getUserConnectionData(connection)~cast[ptr[RefCounter]]; - def ws: SrdRef[WsConnection](rc~cnt, rc~cnt.managedObj~cast[ptr[WsConnection]]~cnt); - ws.status = WsState.CLOSED; - - this.onClose(ws , ws.closeCode , ws.closeReason); - - // reduce counter by one cause we was increment it by one when we initialize the reference to make sure that the object - // will not be released before we closing the connection - - ws.refCounter.count--; - } - - handler this.onConnect(connecting: SrdRef[WsConnection]) : Int as_ptr { - return 0; - } - - handler this.onReady(connecting: SrdRef[WsConnection]) : Void as_ptr {} - handler this.onData(connecting: SrdRef[WsConnection] , data : ref[String] , isBinary : Bool) as_ptr {} - handler this.onClose(connecting: SrdRef[WsConnection] , closeCode : Word[16] , closeReason : String) : Void as_ptr {}; - - handler this.setMaxMessageSize (size : ArchInt) { - _maxMessageSize = size; - fragmentBuffer.bufferGrowSize = (_maxMessageSize / 2)~cast[ArchInt]; - } - - handler this.getMaxMessageSize() : ArchInt{ - return this._maxMessageSize; - } -} \ No newline at end of file