From 005e946f43dbe92b3d14ecbbbf9c33600efbc7e5 Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Sun, 28 Jun 2026 07:59:31 +0300 Subject: [PATCH 01/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] (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/32] (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/32] (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/32] (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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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/32] 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 From a824d9ada06d6dc40618c356b829ea0d6b4e77ca Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Sun, 2 Aug 2026 16:23:11 +0300 Subject: [PATCH 24/32] fix(websocket): test and fix compiler crash and memory corruption after design cleanup --- WebPlatform/WsConnection.alusus | 39 ++++++++++++++------------------- WebPlatform/server.alusus | 16 ++++++++------ 2 files changed, 25 insertions(+), 30 deletions(-) diff --git a/WebPlatform/WsConnection.alusus b/WebPlatform/WsConnection.alusus index 141d3dd..a8719b1 100644 --- a/WebPlatform/WsConnection.alusus +++ b/WebPlatform/WsConnection.alusus @@ -5,6 +5,9 @@ enumStringValue[OPENED, "opened"]; enumStringValue[CLOSING, "closing"]; enumStringValue[CLOSED, "closed"]; + + handler this == ref[this_type]: Bool return this.val == value.val; + handler this != ref[this_type]: Bool return this.val != value.val; } class WsConnection { @@ -20,11 +23,6 @@ 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; } @@ -72,10 +70,9 @@ 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(0) = (statusCode >> 8) & 0xFF; // status code, high byte payload(1) = statusCode & 0xFF; // status code, low byte if (reasonLen > 0) { @@ -84,11 +81,9 @@ this.status = WsStatus.CLOSING; - if Http.writeToWebSocket(this.connection, 8, payload~ptr, payloadLen) <= { + if Http.writeToWebSocket(this.connection, 8, payload~ptr, payloadLen) <= 0 { this.status = WsStatus.CLOSED; } - - return result; } // Called internally by the library to respond to a client-initiated @@ -118,25 +113,23 @@ 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.onReady() : Void as_ptr { } - handler this.onData(bits: Int, data: CharsPtr, dataLen: ArchWord): Int { + handler this.onFrame(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]; + def data_Len : ArchInt = dataLen~cast[ArchInt]; if opcode == 1 or opcode == 2 { if (this.isFragmenting) { @@ -147,16 +140,16 @@ this.fragmentOpcode = opcode; this.fragmentBuffer.clear(); - if (dataLen > this.getMaxMessageSize()) { + if (data_Len > this.getMaxMessageSize()) { this.close(1009, "Message too big"); return 0; } - this.fragmentBuffer.append(data, dataLen); + this.fragmentBuffer.append(data, data_Len); if (fin) { def isBinary : Bool = this.fragmentOpcode == 2; - this.onUserData(this.fragmentBuffer.string, isBinary); + this.onData(this.fragmentBuffer.string, isBinary); this.fragmentBuffer.clear(); } else { this.isFragmenting = 1; @@ -170,16 +163,16 @@ return 0; } - if (this.fragmentBuffer.getLength() + dataLen > this.getMaxMessageSize()) { + if (this.fragmentBuffer.getLength() + data_Len > this.getMaxMessageSize()) { this.close(1009, "Message too big"); return 0; } - this.fragmentBuffer.append(data, dataLen); + this.fragmentBuffer.append(data, data_Len); if (fin) { def isBinary : Bool = this.fragmentOpcode == 2; - this.onUserData(this.fragmentBuffer.string, isBinary); + this.onData(this.fragmentBuffer.string, isBinary); this.fragmentBuffer.clear(); this.isFragmenting = 0; } @@ -187,7 +180,7 @@ if opcode == 8 { // reply with a close frame before tearing down — completes the handshake properly - if dataLen~cast[Int] >= 2 { + if data_Len~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 @@ -203,7 +196,7 @@ return 1; } - handler this.onUserData(data: ref[String], isBinary: Bool) as_ptr { + handler this.onData(data: ref[String], isBinary: Bool) : Void as_ptr { } handler this.onClose() as_ptr { diff --git a/WebPlatform/server.alusus b/WebPlatform/server.alusus index 85307ea..2fb6842 100644 --- a/WebPlatform/server.alusus +++ b/WebPlatform/server.alusus @@ -558,20 +558,22 @@ func wsConnectCallback [WsConnClass: type] (connection: ptr[Http.Connection], userData: ptr[Void]): Int { def ws: SrdRef[WsConnClass]; - ws.alloc()~init(connection); + ws.alloc()~init(); + ws.connection = connection; + ws.status = WsStatus.CONNECTING; 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; + Http.setUserConnectionData(connection, ws.refCounter~ptr~cast[ptr[RefCounter]]); 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]); + def ws: SrdRef[WsConnClass](rc~cnt, rc~cnt.managedObj~cast[ptr[WsConnClass]]~cnt); ws.status = WsStatus.OPENED; ws.onReady(); } @@ -580,13 +582,13 @@ 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); + def ws: SrdRef[WsConnClass](rc~cnt, rc~cnt.managedObj~cast[ptr[WsConnClass]]~cnt); + return ws.onFrame(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]); + def ws: SrdRef[WsConnClass](rc~cnt, rc~cnt.managedObj~cast[ptr[WsConnClass]]~cnt); ws.status = WsStatus.CLOSED; ws.onClose(); @@ -626,7 +628,7 @@ 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 modifier: ref[Core.Basic.TiObject](Spp.astMgr.findModifierForElement(element, enKwd, translations)); def endpointParams: Array[String]; Spp.astMgr.getModifierStringParams(modifier, endpointParams); return endpointParams; From 5d152365eff9a5448a141f7a827b377383aa3860 Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Sun, 2 Aug 2026 16:39:12 +0300 Subject: [PATCH 25/32] feat(websocket): expose connection pointer to onConnect handler for handshake access --- WebPlatform/WsConnection.alusus | 2 +- WebPlatform/server.alusus | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/WebPlatform/WsConnection.alusus b/WebPlatform/WsConnection.alusus index a8719b1..0cd693e 100644 --- a/WebPlatform/WsConnection.alusus +++ b/WebPlatform/WsConnection.alusus @@ -115,7 +115,7 @@ } } - handler this.onConnect(): Int as_ptr { + handler this.onConnect(connection : ptr[Http.Connection]): Int as_ptr { return 0; } diff --git a/WebPlatform/server.alusus b/WebPlatform/server.alusus index 2fb6842..998843f 100644 --- a/WebPlatform/server.alusus +++ b/WebPlatform/server.alusus @@ -568,7 +568,7 @@ ws.refCounter.count++; Http.setUserConnectionData(connection, ws.refCounter~ptr~cast[ptr[RefCounter]]); - return ws.onConnect(); + return ws.onConnect(ws.connection); } func wsReadyCallback [WsConnClass: type] (connection: ptr[Http.Connection], userData: ptr[Void]): Void { From 2f8591f69c651dcd798c612e3d0c966695adabdd Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Sun, 2 Aug 2026 19:48:14 +0300 Subject: [PATCH 26/32] fix(websocket): setMaxMessageSize handler failed to compile due to missing . --- WebPlatform/WsConnection.alusus | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebPlatform/WsConnection.alusus b/WebPlatform/WsConnection.alusus index 0cd693e..2dce50d 100644 --- a/WebPlatform/WsConnection.alusus +++ b/WebPlatform/WsConnection.alusus @@ -29,7 +29,7 @@ handler this.setMaxMessageSize(size: ArchInt) { this.maxMessageSize = size; - fragmentBuffer.bufferGrowSize = (maxMessageSize / 2)~cast[ArchInt]; + this.fragmentBuffer.bufferGrowSize = (this.maxMessageSize / 2)~cast[ArchInt]; } handler this.getMaxMessageSize(): ArchInt{ From 99ab063390f2831374ccb5db6fae4794909c9fa3 Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Sun, 2 Aug 2026 19:59:02 +0300 Subject: [PATCH 27/32] docs(websocket): create Arabic and English documents --- Doc/ws_endpoints.ar.md | 284 +++++++++++++++++++++++++++++++++++++++++ Doc/ws_endpoints.en.md | 176 +++++++++++++++++++++++++ 2 files changed, 460 insertions(+) create mode 100644 Doc/ws_endpoints.ar.md create mode 100644 Doc/ws_endpoints.en.md diff --git a/Doc/ws_endpoints.ar.md b/Doc/ws_endpoints.ar.md new file mode 100644 index 0000000..2a4cbfd --- /dev/null +++ b/Doc/ws_endpoints.ar.md @@ -0,0 +1,284 @@ +# مـنصة_ويب (WebPlatform) + +

+ +[[English]](websocketEndpoint.md) + +[[رجوع]](README.md) + +## إنشاء منافذ المقبس (WebSocket) + +يمكنك إنشاء منفذ مقبس بكتابة صنف عادي، وإضافة المبدل `منفذ_مقبس` (`wsEndpoint`) له مع تحديد المسار +الذي سيستمع عليه، ثم حقن `اتـصال_مقبس` (`WsConnection`) فيه باستخدام المبدل `@حقنة`. + +``` +@منفذ_مقبس["/chat"] +صنف مقبس_الدردشة { + @حقنة عرف اتصال_مقبس: اتـصال_مقبس؛ +} +``` + +
+ +``` +@wsEndpoint["/chat"] +class Chatwebsocket { + @injection def WsConnection: WsConnection; +} +``` + +
+ +الآن أصبح لديك منفذ مقبس يستمع على `/chat`. يُنشأ نموذج جديد من `مقبس_الدردشة` لكل اتصال عميل، +ويبقى حيًّا طوال مدة ذلك الاتصال. + +## تخصيص المعالجات (Overriding Handlers) + +يمنحك `اتـصال_مقبس` أربعة معالجات يمكنك تخصيصها للتفاعل مع دورة حياة الاتصال: + +* `عند_الاتصال` (`onConnect`): يُستدعى عندما يبدأ العميل المصافحة لإنشاء اتصال. يستقبل المؤشر الخام + `اتصال: مؤشر[بـننف.اتـصال]`، والذي يمكنك استخدامه لفحص طلب المصافحة (الترويسات، سلسلة الاستعلام، + إلخ) عبر `بـننف.هات_معلومات_الطلب(اتصال)` . لا تحتفظ بهذا المؤشر لاستخدامه لاحقًا؛ فهو مخصص للقراءة من داخل + `عند_الاتصال` نفسها فقط. أرجع `0` لقبول الاتصال، أو `1` لرفضه. + +* `عند_الجهوزية` (`onReady`): يُستدعى بعد انتهاء المصافحة وفتح الاتصال. هذه أول نقطة يمكنك فيها + إرسال البيانات. + +* `عند_استلام_بيانات` (`onData`): يُستدعى مرة واحدة لكل رسالة كاملة (نصية أو ثنائية)، بعد إعادة + تجميع أي تجزئة عبر عدة إطارات داخليًا. + +* `عند_الإغلاق` (`onClose`): يُستدعى بعد إغلاق الاتصال. + +بما أن هذه المعالجات موجودة على `اتـصال_مقبس` المحقون، فإنك تخصصها بهذه الصيغة، مطابقةً التوقيع +الأصلي تمامًا: + +``` +عملية (هذا: اتـصال_مقبس).عند_الاتصال(اتصال: مؤشر[بـننف.اتـصال]) : صـحيح حدد_مؤشر { + مـتم.طـرفية.اطبع("طريقة الطلب: %s\ج"، بـننف.هات_معلومات_الطلب(اتصال)~محتوى.طريقة_الطلب)؛ + مـتم.طـرفية.اطبع("لدينا اتصال جديد الآن\ج")؛ + أرجع 0؛ +} +``` + +
+ +``` +handler (this: WsConnection).onConnect(connection : ptr[Http.Connection]) : Int set_ptr { + Console.print("request method: %s\n", Http.getRequestInfo(connection)~cnt.requestMethod); + Console.print("we have new connection now\n"); + return 0; +} +``` + +
+ +يخبر `(هذا: اتـصال_مقبس)` المترجم أنك تقدّم تنفيذًا لأحد معالجات `اتـصال_مقبس`، بينما تقوم +`حدد_مؤشر` (`set_ptr`) بتثبيته كالمعالج المستخدم لهذا الصنف بدلًا من المعالج المبدئي. لست مضطرًا +لتخصيص المعالجات الأربعة كلها — أي معالج تتجاهله يُبقي على سلوك `اتـصال_مقبس` المبدئي. + +نجمع كل ذلك معًا: + +``` +@منفذ_مقبس["/chat"] +صنف مقبس_الدردشة { + @حقنة عرف اتصال_مقبس: اتـصال_مقبس؛ + + عملية (هذا: اتـصال_مقبس).عند_الاتصال(اتصال: مؤشر[بـننف.اتـصال]) : صـحيح حدد_مؤشر { + مـتم.طـرفية.اطبع("طريقة الطلب: %s\ج"، بـننف.هات_معلومات_الطلب(اتصال)~محتوى.طريقة_الطلب)؛ + مـتم.طـرفية.اطبع("لدينا اتصال جديد الآن\ج")؛ + أرجع 0؛ + } + + عملية (هذا: اتـصال_مقبس).عند_الجهوزية() : فـراغ حدد_مؤشر { + هذا.أرسل_نصا("مرحبا!")؛ + مـتم.طـرفية.اطبع("المقبس جاهز الآن\ج")؛ + } + + عملية (هذا: اتـصال_مقبس).عند_استلام_بيانات(بيانات: سند[نـص]، ثنائي: Bool) : فـراغ حدد_مؤشر { + مـتم.طـرفية.اطبع("استلمنا هذه البيانات: %s\ج"، بيانات.صوان)؛ + } + + عملية (هذا: اتـصال_مقبس).عند_الإغلاق() : فـراغ حدد_مؤشر { + مـتم.طـرفية.اطبع("تم إغلاق الاتصال\ج")؛ + } +} +``` + +
+ +``` +@wsEndpoint["/chat"] +class Chatwebsocket { + @injection def WsConnection: WsConnection; + + handler (this: WsConnection).onConnect(connection : ptr[Http.Connection]) : Int set_ptr { + Console.print("request method: %s\n", Http.getRequestInfo(connection)~cnt.requestMethod); + Console.print("we have new connection now\n"); + return 0; + } + + handler (this: WsConnection).onReady() : Void set_ptr { + this.sendText("hello!"); + Console.print("the Socket is Ready Now\n"); + } + + handler (this: WsConnection).onData(data: ref[String], isBinary: Bool) : Void set_ptr { + Console.print("we received this data: %s\n", data.buf); + } + + handler (this: WsConnection).onClose() : Void set_ptr { + Console.print("connection closed\n"); + } +} +``` + +
+ +## بيانات مخصصة (Custom Data) + +يمكنك وضع بيانات مخصصة على الصنف، كمعرّف خاص بكل اتصال يصل إلى ذلك المنفذ: + +``` +@منفذ_مقبس["/chat"] +صنف مقبس_الدردشة { + @حقنة عرف اتصال_مقبس: اتـصال_مقبس؛ + + عرف معرف: صـحيح؛ + + عملية (هذا: اتـصال_مقبس).عند_الاتصال(اتصال: مؤشر[بـننف.اتـصال]) : صـحيح حدد_مؤشر { + هذا.معرف = 5؛ + أرجع 0؛ + } +} +``` + +
+ +``` +@wsEndpoint["/chat"] +class Chatwebsocket { + @injection def WsConnection: WsConnection; + + def id : Int; + + handler (this: WsConnection).onConnect(connection : ptr[Http.Connection]) : Int set_ptr { + this.id = 5; + return 0; + } +} +``` + +
+ +## اتـصال_مقبس (WsConnection) + +هذا هو الصنف الذي تحقنه في الصنف الذي تنشئه بالمبدل `منفذ_مقبس`، ويمثل اتصال مقبس واحد. نموذج منه +(عبر صنفك) هو ما تشير إليه `هذا` (`this`) داخل كل معالج تخصصه. + +يمكن أن يكون في إحدى الحالات التالية، معبّرًا عنها بقيم `حـالة_مقبس` (`WsStatus`): + +* `حـالة_مقبس._قيد_الاتصال_` (`WsStatus.CONNECTING`): الحالة المبدئية، حتى تنتهي المصافحة. + +* `حـالة_مقبس._مفتوح_` (`WsStatus.OPENED`): الحالة الوحيدة التي يمكنك فيها إرسال واستقبال إطارات + البيانات؛ كما يمكنك إغلاق الاتصال أثناءها. + +* `حـالة_مقبس._قيد_الإغلاق_` (`WsStatus.CLOSING`): الحالة بين استدعاء `أغلق` (`close`) واستدعاء + المعالج `عند_الإغلاق` (`onClose`). + +* `حـالة_مقبس._مغلق_` (`WsStatus.CLOSED`): الحالة النهائية، وتعني أن الاتصال مغلق. + +#### هات_الحالة (getStatus) + +``` +عملية هذا.هات_الحالة(): حـالة_مقبس؛ +``` + +
+ +``` +handler this.getStatus(): WsStatus; +``` + +
+ +يُرجع الحالة الحالية للاتصال. + +#### أرسل_نصا (sendText) + +``` +عملية هذا.أرسل_نصا(بيانات: مـؤشر_محارف)؛ +``` + +
+ +``` +handler this.sendText(data: CharsPtr); +``` + +
+ +يُرسل رسالة نصية عبر الاتصال. + +#### أرسل_ثنائيا (sendBinary) + +``` +عملية هذا.أرسل_ثنائيا(بيانات: مـؤشر_محارف، طول_البيانات: طـبيعي_متكيف)؛ +``` + +
+ +``` +handler this.sendBinary(data: CharsPtr, dataLen: ArchWord); +``` + +
+ +يُرسل بيانات ثنائية عبر الاتصال. + +* `طول_البيانات` (`dataLen`): طول `بيانات` (`data`) بالبايتات. + +يتحقق كل من `أرسل_نصا` و`أرسل_ثنائيا` أولًا من أن الاتصال ما يزال مفتوحًا قبل إرسال أي بيانات؛ +فإن كان الاتصال مغلقًا، فلا يحدث شيء. + +#### أغلق (close) + +``` +عملية هذا.أغلق()؛ +عملية هذا.أغلق(رمز_الحالة: word[16]، رسالة_السبب: مـؤشر_محارف)؛ +``` + +
+ +``` +handler this.close(); +handler this.close(statusCode: word[16], reasonMessage: CharsPtr); +``` + +
+ +يُغلق الاتصال: ينقل الحالة إلى `حـالة_مقبس._قيد_الإغلاق_`، ويرسل إطار إغلاق للعميل؛ ويُستدعى +`عند_الإغلاق` بمجرد اكتمال مصافحة الإغلاق. + +* الصيغة بلا معطيات تُغلق الاتصال برمز حالة `1000` (إغلاق عادي) ودون رسالة سبب. +* الصيغة الثانية تتيح لك تحديد رمز حالة ورسالة سبب. يجب أن تكون الرموز الخاصة بالتطبيق ضمن المجال + `4000` إلى `4999` — راجع + [مواصفة RFC 6455 لرموز الحالة](https://datatracker.ietf.org/doc/html/rfc6455#section-7.4.2). + +#### الحد الأقصى لحجم الرسالة (Message size limit) + +بشكل مبدئي، يُغلق الاتصال برمز `1009` ("Message too big") إذا تجاوزت رسالة واردة — مجمّعةً عبر كل +أجزائها — 1024 بايت. يمكنك تغيير هذا الحد لكل اتصال، مثلًا من داخل `عند_الاتصال`: + +``` +عملية هذا.حدد_الحجم_الأقصى_للرسالة(الحجم: صـحيح_متكيف)؛ +``` + +
+ +``` +handler this.setMaxMessageSize(size: ArchInt); +``` + +
+ +
diff --git a/Doc/ws_endpoints.en.md b/Doc/ws_endpoints.en.md new file mode 100644 index 0000000..274d5b5 --- /dev/null +++ b/Doc/ws_endpoints.en.md @@ -0,0 +1,176 @@ +# WebPlatform + +[[عربي]](websocketEndpoint.ar.md) + +[[Back]](../README.md) + +## Creating WebSocket Endpoints + +You can create a websocket endpoint by writing a regular class, adding the `wsEndpoint` modifier to +it with the URI it should listen on, and injecting `WsConnection` into it using the `@injection` +modifier. + +``` +@wsEndpoint["/chat"] +class Chatwebsocket { + @injection def WsConnection: WsConnection; +} +``` + +Now you have a websocket endpoint listening at `/chat`. A new instance of `Chatwebsocket` is created +for every client connection and stays alive for the lifetime of that connection. + +## Overriding Handlers + +`WsConnection` gives you four handlers you can override to react to the connection's lifecycle: + +* `onConnect`: called when a client starts the handshake to establish a connection. It receives the + raw `connection: ptr[Http.Connection]`, which you can use to inspect the handshake request (headers, + query string, etc.) via `Http.getRequestInfo(connection)`. Don't store this pointer for later use; it's only meant to be read from within + `onConnect` itself. Return `0` to accept the connection, or `1` to reject it. + +* `onReady`: called once the handshake is done and the connection is open. This is the first point + at which you can send data. + +* `onData`: called once per complete message (text or binary), after any fragmentation across + multiple frames has been reassembled internally. + +* `onClose`: called after the connection has been closed. + +Because these handlers live on the injected `WsConnection`, you override them with this syntax, +matching the original signature exactly: + +``` +handler (this: WsConnection).onConnect(connection : ptr[Http.Connection]) : Int set_ptr { + Console.print("request method: %s\n", Http.getRequestInfo(connection)~cnt.requestMethod); + Console.print("we have new connection now\n"); + return 0; +} +``` + +`(this: WsConnection)` tells the compiler you're providing an implementation for one of +`WsConnection`'s handlers, and `set_ptr` installs it as the one used for this class instead of the +default. You don't have to override all four — any handler you skip keeps `WsConnection`'s default +behavior. + +Putting it together: + +``` +@wsEndpoint["/chat"] +class Chatwebsocket { + @injection def WsConnection: WsConnection; + + handler (this: WsConnection).onConnect(connection : ptr[Http.Connection]) : Int set_ptr { + Console.print("request method: %s\n", Http.getRequestInfo(connection)~cnt.requestMethod); + Console.print("we have new connection now\n"); + return 0; + } + + handler (this: WsConnection).onReady() : Void set_ptr { + this.sendText("hello!"); + Console.print("the Socket is Ready Now\n"); + } + + handler (this: WsConnection).onData(data: ref[String], isBinary: Bool) : Void set_ptr { + Console.print("we received this data: %s\n", data.buf); + } + + handler (this: WsConnection).onClose() : Void set_ptr { + Console.print("connection closed\n"); + } +} +``` + +## Custom Data + +You can set custom data on the class, for example a custom id for each connection made to that +endpoint: + +``` +@wsEndpoint["/chat"] +class Chatwebsocket { + @injection def WsConnection: WsConnection; + + def id : Int; + + handler (this: WsConnection).onConnect(connection : ptr[Http.Connection]) : Int set_ptr { + this.id = 5; + return 0; + } + + handler (this: WsConnection).onClose() : Void set_ptr { + Console.print("the connection with id %i is closed\n", this.id); + } +} +``` + +## WsConnection + +This is the class you inject into the class you create with the `wsEndpoint` modifier, representing +a single websocket connection. An instance of it (through your class) is what `this` refers to +inside every handler you override. + +It can be in one of the following statuses, exposed as `WsStatus` values: + +* `WsStatus.CONNECTING`: the initial status, until the handshake completes. + +* `WsStatus.OPENED`: the only status in which you can send and receive data frames; you can also + close the connection while in this status. + +* `WsStatus.CLOSING`: the status between calling `close` and the `onClose` handler being invoked. + +* `WsStatus.CLOSED`: the final status, indicating the connection is closed. + +#### getStatus + +``` +handler this.getStatus(): WsStatus; +``` + +Returns the connection's current status. + +#### sendText + +``` +handler this.sendText(data: CharsPtr); +``` + +Sends a text message through the connection. + +#### sendBinary + +``` +handler this.sendBinary(data: CharsPtr, dataLen: ArchWord); +``` + +Sends binary data through the connection. + +* `dataLen`: the length of `data`, in bytes. + +`sendText` and `sendBinary` both check if the connection is still opened before send any data. +in cause that the connection is closed then nothing happened. + +#### close + +``` +handler this.close(); +handler this.close(statusCode: word[16], reasonMessage: CharsPtr); +``` + +Closes the connection: moves the status to `WsStatus.CLOSING` and sends a close frame to the +client; `onClose` fires once the close handshake completes. + +* The no-argument form closes with status code `1000` (normal closure) and no reason message. +* The second form lets you specify a status code and a reason message. Application-defined codes + must be in the `4000`-`4999` range — see the + [RFC 6455 spec on status codes](https://datatracker.ietf.org/doc/html/rfc6455#section-7.4.2). + +#### Message size limit + +By default, a connection closes with code `1009` ("Message too big") if an incoming message — +assembled across all its fragments — exceeds 1024 bytes. You can change this limit per connection, +for example from `onConnect`: + +``` +handler this.setMaxMessageSize(size: ArchInt); +``` From 242de954fc754e174cf31f7f7838b6207c19a9f1 Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Sun, 2 Aug 2026 19:59:55 +0300 Subject: [PATCH 28/32] feat(websocket): add Arabic aliases for WsConnection and WsStatus --- ...\331\212\330\250.\330\243\330\263\330\263" | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git "a/\331\205\331\200\331\206\330\265\330\251_\331\210\331\212\330\250.\330\243\330\263\330\263" "b/\331\205\331\200\331\206\330\265\330\251_\331\210\331\212\330\250.\330\243\330\263\330\263" index eddc4a2..ffb7c1d 100644 --- "a/\331\205\331\200\331\206\330\265\330\251_\331\210\331\212\330\250.\330\243\330\263\330\263" +++ "b/\331\205\331\200\331\206\330\265\330\251_\331\210\331\212\330\250.\330\243\330\263\330\263" @@ -99,6 +99,32 @@ عرف مسار_البناء: لقب buildPath؛ } + عرف حـالة_مقبس: لقب WsStatus؛ + @دمج صنف حـالة_مقبس { + عرف _قيد_الاتصال_: لقب CONNECTING؛ + عرف _مفتوح_: لقب OPENED؛ + عرف _قيد_الإغلاق_: لقب CLOSING؛ + عرف _مغلق_: لقب CLOSED؛ + } + + عرف اتـصال_مقبس: لقب WsConnection؛ + @دمج صنف اتـصال_مقبس { + عرف الحالة: لقب status؛ + عرف رمز_الإغلاق: لقب closeCode؛ + عرف سبب_الإغلاق: لقب closeReason؛ + عرف الحجم_الأقصى_للرسالة: لقب maxMessageSize؛ + عرف هات_الحالة: لقب getStatus؛ + عرف حدد_الحجم_الأقصى_للرسالة: لقب setMaxMessageSize؛ + عرف هات_الحجم_الأقصى_للرسالة: لقب getMaxMessageSize؛ + عرف أرسل_نصا: لقب sendText؛ + عرف أرسل_ثنائيا: لقب sendBinary؛ + عرف أغلق: لقب close؛ + عرف عند_الاتصال: لقب onConnect؛ + عرف عند_الجهوزية: لقب onReady؛ + عرف عند_استلام_بيانات: لقب onData؛ + عرف عند_الإغلاق: لقب onClose؛ + } + عرف حـمولة_تحريك_المؤشر: لقب MouseMovePayload؛ @دمج صنف حـمولة_تحريك_المؤشر { عرف موقع_س: لقب posX؛ From 3a553d725a6e375fd33578299361e200f32e5f64 Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Mon, 3 Aug 2026 19:29:52 +0300 Subject: [PATCH 29/32] feat(websocket): add browser-side WebSocket client API --- WebPlatform/Utils/WebSocket.alusus | 182 +++++++++++++++++++++++++++++ WebPlatform/browser_api.alusus | 11 ++ api.js | 135 +++++++++++++++++++++ 3 files changed, 328 insertions(+) create mode 100644 WebPlatform/Utils/WebSocket.alusus diff --git a/WebPlatform/Utils/WebSocket.alusus b/WebPlatform/Utils/WebSocket.alusus new file mode 100644 index 0000000..9bb6eb0 --- /dev/null +++ b/WebPlatform/Utils/WebSocket.alusus @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2026 Sarmad Abdullah + * + * This file is part of Alusus WebPlatform library. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +@merge module WebPlatform { + class WsCloseInfo { + def code: Int; + def reason: String; + def wasClean: Bool; + } + + class WsMessageInfo { + def data: String; + def isBinary: Bool; + } + + class WebSocket { + def socketId: ArchInt(0); + def handlerId: ArchInt(0); + def onOpen: Signal[WebSocket, Int]; + def onMessage: Signal[WebSocket, WsMessageInfo]; + def onError: Signal[WebSocket, Int]; + def onClose: Signal[WebSocket, WsCloseInfo]; + + // Closes the underlying socket (if any) and immediately removes our + // dispatch entry, without waiting for the real 'websocketClose' event + // to round-trip back. Used when `this` itself is about to stop being + // valid for the current connection — either because the whole object + // is being destroyed (~terminate) or because it's about to be reused + // for a brand new connection (connect). In both cases, letting the old + // connection's close event arrive later and fire against `this` would + // be wrong: in the ~terminate case `this` no longer exists, and in the + // reconnect case `this` now represents a different connection and that + // stale event would clobber its socketId/handlerId. + handler this._forceClose() { + if this.socketId != 0 _closeWebSocket(this.socketId, 1000, ""); + def i: ArchInt = findEventHandlerIndex(this.handlerId); + if i != -1 eventHandlers.remove(i); + this.socketId = 0; + this.handlerId = 0; + } + + handler this~terminate() { + this._forceClose(); + } + + handler this.connect(url: ptr[Char]): Bool { + return this.connect(url, ""); + } + + handler this.connect(url: ptr[Char], protocols: ptr[Char]): Bool { + this._forceClose(); + def index: ArchInt = addEventHandler(closure (json: Json) { + def eventName: String = json("eventName"); + def data: Json = json("eventData"); + if eventName == "websocketOpen" { + this.onOpen.emit(this, Int(0)); + } else if eventName == "websocketMessage" { + def info: WsMessageInfo; + info.isBinary = data("isBinary"); + if info.isBinary { + // The actual bytes live in a JS-side map, keyed by + // dataId — they couldn't survive the JSON round-trip + // fetchNextEvent uses, so we pull them into wasm memory + // ourselves via copyWebSocketBinaryData, then let + // String's copying (ptr, length) constructor take + // ownership of a copy before freeing our temp buffer. + def dataLen: ArchWord = data("dataLen"); + def dataId: ArchInt = data("dataId"); + def buffer: ptr[array[Char]] = Memory.alloc(dataLen)~cast[ptr[array[Char]]]; + copyWebSocketBinaryData(dataId, buffer~cast[ptr[Char]]); + info.data = String(buffer, dataLen~cast[ArchInt]); + Memory.free(buffer); + } else { + info.data = data("data"); + } + this.onMessage.emit(this, info); + } else if eventName == "websocketError" { + this.onError.emit(this, Int(0)); + } else if eventName == "websocketClose" { + def info: WsCloseInfo; + info.code = data("code"); + info.reason = data("reason"); + info.wasClean = data("wasClean"); + this.onClose.emit(this, info); + this.socketId = 0; + this.handlerId = 0; + } + }); + this.handlerId = eventHandlers(index).id; + this.socketId = _createWebSocket(url, protocols, this.handlerId); + if this.socketId == 0 { + // Synchronous construction failure (invalid URL, bad protocols, + // per the WebSocket constructor's SyntaxError cases) — a caller + // mistake, not a connection-level error. The native API doesn't + // fire its 'error' event for this case either (no object was + // ever created to fire an event on), so we don't emit onError + // here; the caller finds out from this return value instead. + def i: ArchInt = findEventHandlerIndex(this.handlerId); + if i != -1 eventHandlers.remove(i); + this.handlerId = 0; + return false; + } + return true; + } + + handler this.send(data: ptr[Char]): Bool { + if this.socketId == 0 return false; + return _sendWebSocketMessage(this.socketId, data); + } + + handler this.sendBinary(data: ptr[Char], dataLen: ArchWord): Bool { + if this.socketId == 0 return false; + return _sendWebSocketBinary(this.socketId, data, dataLen); + } + + handler this.close(): Bool { + return this.close(1000, ""); + } + + handler this.close(code: Int): Bool { + return this.close(code, ""); + } + + handler this.close(code: Int, reason: ptr[Char]): Bool { + if this.socketId == 0 return false; + // Can fail synchronously here too (MDN: InvalidAccessError if code + // isn't 1000 or in 3000-4999, SyntaxError if reason exceeds 123 + // UTF-8 bytes — the same caller-mistake-vs-connection-error split + // as connect(), so this reports through the return value, not + // onError). + if not _closeWebSocket(this.socketId, code, reason) return false; + // Unlike _forceClose, we deliberately don't touch eventHandlers or + // socketId/handlerId here: `this` is still a valid, still-current + // connection, and the real 'websocketClose' event will arrive + // asynchronously — it needs to still find its handler registered + // so onClose fires normally. That event's branch above already + // resets socketId/handlerId once it arrives. + return true; + } + + handler this.getState(): Int { + if this.socketId == 0 return -1; + return getWebSocketState(this.socketId); + } + + handler this.getUrl(): ptr[array[Char]] { + if this.socketId == 0 return 0; + return getWebSocketUrl(this.socketId); + } + + handler this.getProtocol(): ptr[array[Char]] { + if this.socketId == 0 return 0; + return _getWebSocketProtocol(this.socketId); + } + + handler this.getExtensions(): ptr[array[Char]] { + if this.socketId == 0 return 0; + return _getWebSocketExtensions(this.socketId); + } + + handler this.getBufferedAmount(): ArchWord { + if this.socketId == 0 return 0; + return _getWebSocketBufferedAmount(this.socketId); + } + } +} diff --git a/WebPlatform/browser_api.alusus b/WebPlatform/browser_api.alusus index 4d6ce8d..306390a 100644 --- a/WebPlatform/browser_api.alusus +++ b/WebPlatform/browser_api.alusus @@ -66,6 +66,17 @@ @expname[stopTimer] function _stopTimer (id: Word); @expname[setTimeout] function _setTimeout (duration: Word, cbId: ArchInt): Word; @expname[cancelTimeout] function _cancelTimeout (id: Word); + // WebSockets + @expname[createWebSocket] function _createWebSocket (url: ptr[Char], protocols: ptr[Char], cbId: ArchInt): ArchInt; + @expname[sendWebSocketMessage] function _sendWebSocketMessage (socketId: ArchInt, data: ptr[Char]): Bool; + @expname[sendWebSocketBinary] function _sendWebSocketBinary (socketId: ArchInt, data: ptr[Char], dataLen: ArchWord): Bool; + @expname[copyWebSocketBinaryData] function copyWebSocketBinaryData (dataId: ArchInt, dest: ptr[Char]); + @expname[closeWebSocket] function _closeWebSocket (socketId: ArchInt, code: Int, reason: ptr[Char]): Bool; + @expname[getWebSocketState] function getWebSocketState (socketId: ArchInt): Int; + @expname[getWebSocketUrl] function getWebSocketUrl (socketId: ArchInt): ptr[array[Char]]; + @expname[getWebSocketProtocol] function _getWebSocketProtocol (socketId: ArchInt): ptr[array[Char]]; + @expname[getWebSocketExtensions] function _getWebSocketExtensions (socketId: ArchInt): ptr[array[Char]]; + @expname[getWebSocketBufferedAmount] function _getWebSocketBufferedAmount (socketId: ArchInt): ArchWord; // Resource Management @expname[loadImage] function _loadImage (url: ptr[Char], cbId: ArchInt); @expname[getImageDimensions] function getImageDimensions (imgId: Int, result: ref[Dimensions]); diff --git a/api.js b/api.js index 71f90ea..0db2f4f 100644 --- a/api.js +++ b/api.js @@ -21,11 +21,15 @@ const STACK_SIZE = 8192; const wasmApi = {}; const eventsQueue = []; const resources = {}; +const webSockets = {}; +const webSocketBinaryData = {}; const requestControllers = {}; const audioContext = new (window.AudioContext || window.webkitAudioContext)(); audioContext.resume(); let resourceCounter = 0; +let webSocketCounter = 0; +let webSocketBinaryDataCounter = 0; let requestControllerCounter = 0; let program; let wasmMemory = null; @@ -436,6 +440,133 @@ wasmApi.cancelTimeout = (id) => { clearTimeout(id); } +// WebSocket APIs + +wasmApi.createWebSocket = (url , protocols , cbId) => { + const jsProtocols = toJsString(protocols); + let ws; + try { + ws = jsProtocols ? new WebSocket(toJsString(url), jsProtocols) : new WebSocket(toJsString(url)); + } catch (err) { + console.error('WebSocket construction failed:', err); + return 0; + } + // Alusus has no concept of a JS Blob (wasm can only ever receive raw + // bytes/ids, never a live object reference), so binary messages always end + // up converted to raw bytes on our side regardless of binaryType. Default + // to 'arraybuffer' so that conversion is synchronous instead of paying for + // an extra Blob.arrayBuffer() microtask on every binary message; onmessage + // below still handles a Blob correctly if something sets it back later. + ws.binaryType = 'arraybuffer'; + const socketId = ++webSocketCounter; + webSockets[socketId] = ws; + + ws.onopen = () => { + onEvent(cbId, true, 'websocketOpen', {}); + }; + + ws.onmessage = (event) => { + if (typeof event.data === 'string') { + onEvent(cbId, true, 'websocketMessage', { data: event.data, isBinary: false }); + return; + } + // Binary message: ArrayBuffer (cause we will set the binaryType as 'arraybuffer'). + // the raw bytes can't survive the JSON.stringify round-trip fetchNextEvent + // uses for every other event, so instead of embedding the data itself, + // we stash it here and hand Alusus back a small id + length; it then + // pulls the actual bytes into wasm memory via copyWebSocketBinaryData, + // the same "write into caller-provided memory" pattern used elsewhere + // (e.g. getElementDimensions). + + const bytes = new Uint8Array(event.data); + const dataId = ++webSocketBinaryDataCounter; + webSocketBinaryData[dataId] = bytes; + onEvent(cbId, true, 'websocketMessage', { isBinary: true, dataId, dataLen: bytes.length }); + }; + + ws.onerror = () => { + onEvent(cbId, true, 'websocketError', {}); + }; + + ws.onclose = (event) => { + onEvent(cbId, false, 'websocketClose', { + code: event.code, + reason: event.reason, + wasClean: event.wasClean + }); + delete webSockets[socketId]; + }; + + return socketId; +} + +wasmApi.sendWebSocketMessage = (socketId, data) => { + const ws = webSockets[socketId]; + if (!ws || ws.readyState !== WebSocket.OPEN) return false; + try { + ws.send(toJsString(data)); + return true; + } catch (err) { + console.error('WebSocket send failed:', err); + return false; + } +}; + +wasmApi.sendWebSocketBinary = (socketId, dataPtr, dataLen) => { + const ws = webSockets[socketId]; + if (!ws || ws.readyState !== WebSocket.OPEN) return false; + try { + ws.send(new Uint8Array(wasmMemory.buffer, dataPtr, dataLen)); + return true; + } catch (err) { + console.error('WebSocket sendBinary failed:', err); + return false; + } +}; + +wasmApi.copyWebSocketBinaryData = (dataId, destPtr) => { + const bytes = webSocketBinaryData[dataId]; + if (!bytes) return; + new Uint8Array(wasmMemory.buffer, destPtr, bytes.length).set(bytes); + delete webSocketBinaryData[dataId]; +}; + +wasmApi.closeWebSocket = (socketId, code, reason) => { + const ws = webSockets[socketId]; + if (!ws) return false; + try { + ws.close(code, toJsString(reason)); + return true; + } catch (err) { + console.error('WebSocket close failed:', err); + return false; + } +}; + +wasmApi.getWebSocketState = (socketId) => { + const ws = webSockets[socketId]; + return ws ? ws.readyState : -1; +}; + +wasmApi.getWebSocketUrl = (socketId) => { + const ws = webSockets[socketId]; + return ws ? toWasmString(ws.url) : 0; +}; + +wasmApi.getWebSocketProtocol = (socketId) => { + const ws = webSockets[socketId]; + return ws ? toWasmString(ws.protocol) : 0; +}; + +wasmApi.getWebSocketExtensions = (socketId) => { + const ws = webSockets[socketId]; + return ws ? toWasmString(ws.extensions) : 0; +}; + +wasmApi.getWebSocketBufferedAmount = (socketId) => { + const ws = webSockets[socketId]; + return ws ? ws.bufferedAmount : 0; +}; // Resource Management wasmApi.loadImage = (url, cbId) => { @@ -1095,6 +1226,10 @@ const eventPropMap = { loadAudio: ['resourceId', 'success'], loadJsScript: ['success'], sendRequest: ['status', 'headers', 'body'], + websocketOpen: [], + websocketMessage: ['data', 'isBinary', 'dataId', 'dataLen'], + websocketError: [], + websocketClose: ['code', 'reason', 'wasClean'], timer: [], touchstart: pickNeededTouchEventData, touchend: pickNeededTouchEventData, From 683f1170f7983c2f9a7caba12ee21f8396e7bf96 Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Mon, 3 Aug 2026 19:46:52 +0300 Subject: [PATCH 30/32] docs(websocket): fix wrong comment about websocket_timeout_ms --- Doc/ws_endpoints.ar.md | 28 ++++++++++++++++++++++++++++ Doc/ws_endpoints.en.md | 17 +++++++++++++++++ WebPlatform/server.alusus | 19 +++++++++++-------- 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/Doc/ws_endpoints.ar.md b/Doc/ws_endpoints.ar.md index 2a4cbfd..372be76 100644 --- a/Doc/ws_endpoints.ar.md +++ b/Doc/ws_endpoints.ar.md @@ -32,6 +32,34 @@ class Chatwebsocket { الآن أصبح لديك منفذ مقبس يستمع على `/chat`. يُنشأ نموذج جديد من `مقبس_الدردشة` لكل اتصال عميل، ويبقى حيًّا طوال مدة ذلك الاتصال. +## المهل الزمنية (Timeouts) + +عند بدء الخادم (`شغل_الخادم`، `ابدأ_الخادم`، `ابن_وشغل_الخادم`، إلخ)، يمكنك تجاوز القيمة المبدئية +لـ`websocket_timeout_ms` عبر المعطى `الخيارات: مـصفوفة[مـؤشر_محارف]`. يتحكم هذا الخيار في المدة التي +يمكن أن يبقى فيها اتصال المقبس دون استجابة قبل أن يعتبره الخادم غير مستجيب ويغلقه. قيمته المبدئية +`10000` (10 ثوانٍ) إن لم تحددها بنفسك. + +``` +شغل_الخادم[وحدات_الخادم]( + مسار_الموارد_الرئيسي، مسار_المنافذ_المرئية، + مـصفوفة[مـؤشر_محارف]({ "listening_ports"، "8010"، "websocket_timeout_ms"، "30000" }) +)؛ +``` + +
+ +``` +runServer[serverModules]( + mainAssetsPath, uiEndpointsPath, + Array[CharsPtr]({ "listening_ports", "8010", "websocket_timeout_ms", "30000" }) +); +``` + +
+ +لا تُغيّر هذه القيمة بشكل عشوائي — اختبرها أولًا مقابل الحمل المتوقع لتطبيقك قبل الاعتماد على قيمة +مختلفة في الإنتاج. + ## تخصيص المعالجات (Overriding Handlers) يمنحك `اتـصال_مقبس` أربعة معالجات يمكنك تخصيصها للتفاعل مع دورة حياة الاتصال: diff --git a/Doc/ws_endpoints.en.md b/Doc/ws_endpoints.en.md index 274d5b5..cc7cbef 100644 --- a/Doc/ws_endpoints.en.md +++ b/Doc/ws_endpoints.en.md @@ -20,6 +20,23 @@ class Chatwebsocket { Now you have a websocket endpoint listening at `/chat`. A new instance of `Chatwebsocket` is created for every client connection and stays alive for the lifetime of that connection. +## Timeouts + +When you start the server (`runServer`, `startServer`, `buildAndRunServer`, etc.), you can override +`websocket_timeout_ms` through the `options: Array[CharsPtr]` argument. It controls how long a +WebSocket connection can sit without a response before the server treats it as unresponsive and +closes it. It defaults to `10000` (10 seconds) if you don't set it yourself. + +``` +runServer[serverModules]( + mainAssetsPath, uiEndpointsPath, + Array[CharsPtr]({ "listening_ports", "8010", "websocket_timeout_ms", "30000" }) +); +``` + +Don't change this value casually — test it against your own expected load first before relying on a +different value in production. + ## Overriding Handlers `WsConnection` gives you four handlers you can override to react to the connection's lifecycle: diff --git a/WebPlatform/server.alusus b/WebPlatform/server.alusus index 998843f..d34681c 100644 --- a/WebPlatform/server.alusus +++ b/WebPlatform/server.alusus @@ -383,22 +383,25 @@ 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. + // Bounds how long a single network read or write operation (over any HTTP + // connection this server handles) can block on an unresponsive/dead peer + // before CivetWeb gives up and closes it — without this, a stuck read/write + // can tie up a worker thread for a very long time (OS-level TCP timeout) if + // left unset. Defaults to CivetWeb's own 30s if we don't set it. 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. + // Same read/write timeout as request_timeout_ms above, but specifically for + // WebSocket connections — set separately because WebSocket connections are + // expected to sit idle between messages far longer than a normal HTTP + // request should take, so they need a more relaxed value. Falls back to + // request_timeout_ms if unset. 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 From 9865cef025a05725c1f2eb244ec937d749c266fc Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Tue, 4 Aug 2026 20:54:02 +0300 Subject: [PATCH 31/32] refactor(websocket): rework browser client construction and error reporting --- WebPlatform/Utils/WebSocket.alusus | 95 +++++++++++++----------------- WebPlatform/browser_api.alusus | 7 ++- api.js | 45 ++++++-------- 3 files changed, 63 insertions(+), 84 deletions(-) diff --git a/WebPlatform/Utils/WebSocket.alusus b/WebPlatform/Utils/WebSocket.alusus index 9bb6eb0..80e787c 100644 --- a/WebPlatform/Utils/WebSocket.alusus +++ b/WebPlatform/Utils/WebSocket.alusus @@ -37,34 +37,32 @@ def onError: Signal[WebSocket, Int]; def onClose: Signal[WebSocket, WsCloseInfo]; - // Closes the underlying socket (if any) and immediately removes our - // dispatch entry, without waiting for the real 'websocketClose' event - // to round-trip back. Used when `this` itself is about to stop being - // valid for the current connection — either because the whole object - // is being destroyed (~terminate) or because it's about to be reused - // for a brand new connection (connect). In both cases, letting the old - // connection's close event arrive later and fire against `this` would - // be wrong: in the ~terminate case `this` no longer exists, and in the - // reconnect case `this` now represents a different connection and that - // stale event would clobber its socketId/handlerId. - handler this._forceClose() { - if this.socketId != 0 _closeWebSocket(this.socketId, 1000, ""); - def i: ArchInt = findEventHandlerIndex(this.handlerId); - if i != -1 eventHandlers.remove(i); - this.socketId = 0; - this.handlerId = 0; + // Only way to get a WebSocket instance: mirrors `new WebSocket(url)` + // throwing and leaving you with no reference at all on construction + // failure. Returns an empty (isNull() == true) SrdRef if the + // underlying browser constructor rejects synchronously (bad URL, + // malformed protocols); a live, already-connecting instance otherwise. + + func create(url: ptr[Char], protocols: ptr[Char]): SrdRef[WebSocket] { + def ws: SrdRef[WebSocket]; + ws.construct(); + if not ws._connect(url, protocols) ws.release(); + return ws; } - - handler this~terminate() { - this._forceClose(); + func create(url: ptr[Char]): SrdRef[WebSocket] { + return WebSocket.create(url, ""); } - handler this.connect(url: ptr[Char]): Bool { - return this.connect(url, ""); + handler this~terminate() { + if (this.socketId != 0) { + _closeWebSocket(this.socketId, 1000, ""); + deleteWebSocket(this.socketId); + } + def i: ArchInt = findEventHandlerIndex(this.handlerId); + if i != -1 eventHandlers.remove(i); } - handler this.connect(url: ptr[Char], protocols: ptr[Char]): Bool { - this._forceClose(); + handler this._connect(url: ptr[Char], protocols: ptr[Char]): Bool { def index: ArchInt = addEventHandler(closure (json: Json) { def eventName: String = json("eventName"); def data: Json = json("eventData"); @@ -80,10 +78,10 @@ // ourselves via copyWebSocketBinaryData, then let // String's copying (ptr, length) constructor take // ownership of a copy before freeing our temp buffer. - def dataLen: ArchWord = data("dataLen"); - def dataId: ArchInt = data("dataId"); - def buffer: ptr[array[Char]] = Memory.alloc(dataLen)~cast[ptr[array[Char]]]; - copyWebSocketBinaryData(dataId, buffer~cast[ptr[Char]]); + def dataLen: Int = data("dataLen"); + def dataId: Int = data("dataId"); + def buffer: ptr[array[Char]] = Memory.alloc(dataLen~cast[ArchWord])~cast[ptr[array[Char]]]; + copyWebSocketBinaryData(dataId~cast[ArchInt], buffer~cast[ptr[Char]]); info.data = String(buffer, dataLen~cast[ArchInt]); Memory.free(buffer); } else { @@ -98,7 +96,14 @@ info.reason = data("reason"); info.wasClean = data("wasClean"); this.onClose.emit(this, info); - this.socketId = 0; + // socketId intentionally left alone: getState/getUrl/ + // getProtocol/getExtensions/getBufferedAmount all still + // work against a closed socket on the real API, and we + // want the same here. The dispatch entry itself is done + // though, since no further events will ever arrive for a + // closed socket. + def i: ArchInt = findEventHandlerIndex(this.handlerId); + if i != -1 eventHandlers.remove(i); this.handlerId = 0; } }); @@ -119,63 +124,45 @@ return true; } - handler this.send(data: ptr[Char]): Bool { - if this.socketId == 0 return false; + // Returns 0 (null) on success, or a pointer to an error message string + // describing what went wrong. + handler this.sendText(data: ptr[Char]): ptr[array[Char]] { return _sendWebSocketMessage(this.socketId, data); } - handler this.sendBinary(data: ptr[Char], dataLen: ArchWord): Bool { - if this.socketId == 0 return false; + handler this.sendBinary(data: ptr[Char], dataLen: ArchWord): ptr[array[Char]] { return _sendWebSocketBinary(this.socketId, data, dataLen); } - handler this.close(): Bool { + handler this.close(): ptr[array[Char]] { return this.close(1000, ""); } - handler this.close(code: Int): Bool { + handler this.close(code: Int): ptr[array[Char]] { return this.close(code, ""); } - handler this.close(code: Int, reason: ptr[Char]): Bool { - if this.socketId == 0 return false; - // Can fail synchronously here too (MDN: InvalidAccessError if code - // isn't 1000 or in 3000-4999, SyntaxError if reason exceeds 123 - // UTF-8 bytes — the same caller-mistake-vs-connection-error split - // as connect(), so this reports through the return value, not - // onError). - if not _closeWebSocket(this.socketId, code, reason) return false; - // Unlike _forceClose, we deliberately don't touch eventHandlers or - // socketId/handlerId here: `this` is still a valid, still-current - // connection, and the real 'websocketClose' event will arrive - // asynchronously — it needs to still find its handler registered - // so onClose fires normally. That event's branch above already - // resets socketId/handlerId once it arrives. - return true; + handler this.close(code: Int, reason: ptr[Char]): ptr[array[Char]] { + return _closeWebSocket(this.socketId, code, reason); } handler this.getState(): Int { - if this.socketId == 0 return -1; return getWebSocketState(this.socketId); } handler this.getUrl(): ptr[array[Char]] { - if this.socketId == 0 return 0; return getWebSocketUrl(this.socketId); } handler this.getProtocol(): ptr[array[Char]] { - if this.socketId == 0 return 0; return _getWebSocketProtocol(this.socketId); } handler this.getExtensions(): ptr[array[Char]] { - if this.socketId == 0 return 0; return _getWebSocketExtensions(this.socketId); } handler this.getBufferedAmount(): ArchWord { - if this.socketId == 0 return 0; return _getWebSocketBufferedAmount(this.socketId); } } diff --git a/WebPlatform/browser_api.alusus b/WebPlatform/browser_api.alusus index 306390a..265b125 100644 --- a/WebPlatform/browser_api.alusus +++ b/WebPlatform/browser_api.alusus @@ -68,15 +68,16 @@ @expname[cancelTimeout] function _cancelTimeout (id: Word); // WebSockets @expname[createWebSocket] function _createWebSocket (url: ptr[Char], protocols: ptr[Char], cbId: ArchInt): ArchInt; - @expname[sendWebSocketMessage] function _sendWebSocketMessage (socketId: ArchInt, data: ptr[Char]): Bool; - @expname[sendWebSocketBinary] function _sendWebSocketBinary (socketId: ArchInt, data: ptr[Char], dataLen: ArchWord): Bool; + @expname[sendWebSocketMessage] function _sendWebSocketMessage (socketId: ArchInt, data: ptr[Char]): ptr[array[Char]]; + @expname[sendWebSocketBinary] function _sendWebSocketBinary (socketId: ArchInt, data: ptr[Char], dataLen: ArchWord): ptr[array[Char]]; @expname[copyWebSocketBinaryData] function copyWebSocketBinaryData (dataId: ArchInt, dest: ptr[Char]); - @expname[closeWebSocket] function _closeWebSocket (socketId: ArchInt, code: Int, reason: ptr[Char]): Bool; + @expname[closeWebSocket] function _closeWebSocket (socketId: ArchInt, code: Int, reason: ptr[Char]): ptr[array[Char]]; @expname[getWebSocketState] function getWebSocketState (socketId: ArchInt): Int; @expname[getWebSocketUrl] function getWebSocketUrl (socketId: ArchInt): ptr[array[Char]]; @expname[getWebSocketProtocol] function _getWebSocketProtocol (socketId: ArchInt): ptr[array[Char]]; @expname[getWebSocketExtensions] function _getWebSocketExtensions (socketId: ArchInt): ptr[array[Char]]; @expname[getWebSocketBufferedAmount] function _getWebSocketBufferedAmount (socketId: ArchInt): ArchWord; + @expname[deleteWebSocket] function deleteWebSocket (socketId: ArchInt); // Resource Management @expname[loadImage] function _loadImage (url: ptr[Char], cbId: ArchInt); @expname[getImageDimensions] function getImageDimensions (imgId: Int, result: ref[Dimensions]); diff --git a/api.js b/api.js index 0db2f4f..b6468c0 100644 --- a/api.js +++ b/api.js @@ -458,6 +458,7 @@ wasmApi.createWebSocket = (url , protocols , cbId) => { // an extra Blob.arrayBuffer() microtask on every binary message; onmessage // below still handles a Blob correctly if something sets it back later. ws.binaryType = 'arraybuffer'; + const socketId = ++webSocketCounter; webSockets[socketId] = ws; @@ -470,14 +471,7 @@ wasmApi.createWebSocket = (url , protocols , cbId) => { onEvent(cbId, true, 'websocketMessage', { data: event.data, isBinary: false }); return; } - // Binary message: ArrayBuffer (cause we will set the binaryType as 'arraybuffer'). - // the raw bytes can't survive the JSON.stringify round-trip fetchNextEvent - // uses for every other event, so instead of embedding the data itself, - // we stash it here and hand Alusus back a small id + length; it then - // pulls the actual bytes into wasm memory via copyWebSocketBinaryData, - // the same "write into caller-provided memory" pattern used elsewhere - // (e.g. getElementDimensions). - + const bytes = new Uint8Array(event.data); const dataId = ++webSocketBinaryDataCounter; webSocketBinaryData[dataId] = bytes; @@ -494,7 +488,6 @@ wasmApi.createWebSocket = (url , protocols , cbId) => { reason: event.reason, wasClean: event.wasClean }); - delete webSockets[socketId]; }; return socketId; @@ -502,71 +495,69 @@ wasmApi.createWebSocket = (url , protocols , cbId) => { wasmApi.sendWebSocketMessage = (socketId, data) => { const ws = webSockets[socketId]; - if (!ws || ws.readyState !== WebSocket.OPEN) return false; try { ws.send(toJsString(data)); - return true; + return 0; } catch (err) { - console.error('WebSocket send failed:', err); - return false; + return toWasmString(err.message); } }; wasmApi.sendWebSocketBinary = (socketId, dataPtr, dataLen) => { const ws = webSockets[socketId]; - if (!ws || ws.readyState !== WebSocket.OPEN) return false; try { ws.send(new Uint8Array(wasmMemory.buffer, dataPtr, dataLen)); - return true; + return 0; } catch (err) { - console.error('WebSocket sendBinary failed:', err); - return false; + return toWasmString(err.message); } }; wasmApi.copyWebSocketBinaryData = (dataId, destPtr) => { const bytes = webSocketBinaryData[dataId]; - if (!bytes) return; new Uint8Array(wasmMemory.buffer, destPtr, bytes.length).set(bytes); delete webSocketBinaryData[dataId]; }; wasmApi.closeWebSocket = (socketId, code, reason) => { const ws = webSockets[socketId]; - if (!ws) return false; try { ws.close(code, toJsString(reason)); - return true; + return 0; } catch (err) { - console.error('WebSocket close failed:', err); - return false; + return toWasmString(err.message); } }; wasmApi.getWebSocketState = (socketId) => { const ws = webSockets[socketId]; - return ws ? ws.readyState : -1; + return ws.readyState; }; wasmApi.getWebSocketUrl = (socketId) => { const ws = webSockets[socketId]; - return ws ? toWasmString(ws.url) : 0; + return toWasmString(ws.url); }; wasmApi.getWebSocketProtocol = (socketId) => { const ws = webSockets[socketId]; - return ws ? toWasmString(ws.protocol) : 0; + return toWasmString(ws.protocol); }; wasmApi.getWebSocketExtensions = (socketId) => { const ws = webSockets[socketId]; - return ws ? toWasmString(ws.extensions) : 0; + return toWasmString(ws.extensions); }; wasmApi.getWebSocketBufferedAmount = (socketId) => { const ws = webSockets[socketId]; - return ws ? ws.bufferedAmount : 0; + return ws.bufferedAmount; }; + +wasmApi.deleteWebSocket = (socketId) => { + delete webSockets[socketId]; +}; + // Resource Management wasmApi.loadImage = (url, cbId) => { From 26f79d2154db58032ee01a17f24253b40818a5ef Mon Sep 17 00:00:00 2001 From: Hisham Mahgoub Date: Tue, 4 Aug 2026 20:58:05 +0300 Subject: [PATCH 32/32] docs(websocket): add browser WebSocket client API (EN/AR) --- Doc/webSocketApi.ar.md | 416 +++++++++++++++++++++++++++++++++++++++++ Doc/webSocketApi.en.md | 227 ++++++++++++++++++++++ 2 files changed, 643 insertions(+) create mode 100644 Doc/webSocketApi.ar.md create mode 100644 Doc/webSocketApi.en.md diff --git a/Doc/webSocketApi.ar.md b/Doc/webSocketApi.ar.md new file mode 100644 index 0000000..07dccd3 --- /dev/null +++ b/Doc/webSocketApi.ar.md @@ -0,0 +1,416 @@ +# مـنصة_ويب (WebPlatform) + +
+ +[[English]](WebsocketApi.md) + +[[رجوع]](../README.ar.md) + +## مـقبس_ويب (WebSocket Browser Api) + +النظير على جانب المتصفح للصنف `اتـصال_مقبس` (`WsConnection`) الذي يعمل على جانب الخادم. + +اقرأ أولًا توثيق [واجهة WebSocket في MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) +لفهم كائن المتصفح الذي يغلّفه هذا الصنف. + +#### بنية الصنف + +``` +صنف مـقبس_ويب { + عرف عند_الفتح: إشـارة[مـقبس_ويب، صـحيح]؛ + عرف عند_استلام_رسالة: إشـارة[مـقبس_ويب، حـمولة_رسالة_مقبس]؛ + عرف عند_الخطأ: إشـارة[مـقبس_ويب، صـحيح]؛ + عرف عند_الإغلاق: إشـارة[مـقبس_ويب، حـمولة_إغلاق_مقبس]؛ + + دالة أنشئ(الرابط: مؤشر[مـحرف]، البروتوكولات: مؤشر[مـحرف]): سـندنا[مـقبس_ويب]؛ + دالة أنشئ(الرابط: مؤشر[مـحرف]): سـندنا[مـقبس_ويب]؛ + + عملية هذا.أرسل_نصا(بيانات: مؤشر[مـحرف]): مؤشر[مصفوفة[مـحرف]]؛ + عملية هذا.أرسل_ثنائيا(بيانات: مؤشر[مـحرف]، طول_البيانات: طـبيعي_متكيف): مؤشر[مصفوفة[مـحرف]]؛ + + عملية هذا.أغلق(): مؤشر[مصفوفة[مـحرف]]؛ + عملية هذا.أغلق(الرمز: صـحيح): مؤشر[مصفوفة[مـحرف]]؛ + عملية هذا.أغلق(الرمز: صـحيح، السبب: مؤشر[مـحرف]): مؤشر[مصفوفة[مـحرف]]؛ + + عملية هذا.هات_الحالة(): صـحيح؛ + عملية هذا.هات_الرابط(): مؤشر[مصفوفة[مـحرف]]؛ + عملية هذا.هات_البروتوكول(): مؤشر[مصفوفة[مـحرف]]؛ + عملية هذا.هات_الامتدادات(): مؤشر[مصفوفة[مـحرف]]؛ + عملية هذا.هات_الكمية_المخزنة(): طـبيعي_متكيف؛ +} +``` + +
+ +``` +class WebSocket { + def onOpen: Signal[WebSocket, Int]; + def onMessage: Signal[WebSocket, WsMessageInfo]; + def onError: Signal[WebSocket, Int]; + def onClose: Signal[WebSocket, WsCloseInfo]; + + func create(url: ptr[Char], protocols: ptr[Char]): SrdRef[WebSocket]; + func create(url: ptr[Char]): SrdRef[WebSocket]; + + handler this.sendText(data: ptr[Char]): ptr[array[Char]]; + handler this.sendBinary(data: ptr[Char], dataLen: ArchWord): ptr[array[Char]]; + + handler this.close(): ptr[array[Char]]; + handler this.close(code: Int): ptr[array[Char]]; + handler this.close(code: Int, reason: ptr[Char]): ptr[array[Char]]; + + handler this.getState(): Int; + handler this.getUrl(): ptr[array[Char]]; + handler this.getProtocol(): ptr[array[Char]]; + handler this.getExtensions(): ptr[array[Char]]; + handler this.getBufferedAmount(): ArchWord; +} +``` + +
+ +#### إنشاء نموذج مـقبس_ويب + +``` +دالة أنشئ(الرابط: مؤشر[مـحرف]، البروتوكولات: مؤشر[مـحرف]): سـندنا[مـقبس_ويب]؛ +دالة أنشئ(الرابط: مؤشر[مـحرف]): سـندنا[مـقبس_ويب]؛ +``` + +
+ +``` +func create(url: ptr[Char], protocols: ptr[Char]): SrdRef[WebSocket]; +func create(url: ptr[Char]): SrdRef[WebSocket]; +``` + +
+ +تحصل على نموذج باستدعاء `مـقبس_ويب.أنشئ(الرابط)` (`WebSocket.create(url)`) — أو +`مـقبس_ويب.أنشئ(الرابط، البروتوكولات)` (`WebSocket.create(url, protocols)`) لعرض بروتوكولات فرعية — +وهذه هي الطريقة *الوحيدة* للحصول على نموذج؛ لا يوجد بانٍ عمومي (constructor). تُرجع الدالة +`سـندنا[مـقبس_ويب]` (`SrdRef[WebSocket]`). + +تحقق دائمًا مما إذا كان `سـندنا[مـقبس_ويب]` (`SrdRef[WebSocket]`) المُرجَع خاليًا عبر `أهو_عدم` +(`isNull`) قبل استخدامه. سيكون خاليًا إذا حدث خطأ ما أثناء إنشاء كائن `WebSocket` الأصلي في +المتصفح — رابط غير صالح، بروتوكول غير صحيح، بروتوكولات مشوهة، رابط يحتوي على جزء (fragment)، إلخ. + +راجع [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#exceptions) للاطلاع +على القائمة الكاملة لحالات الاستثناء. +هذا يحاكي الواجهة الحقيقية: إن رمى بانِي المتصفح استثناءً، فلن تحصل على كائن على الإطلاق، لذا فإن +هذه المكتبة لا تعطيك أبدًا نموذجًا معطوبًا هي الأخرى — تقوم `أنشئ` (`create`) ببناء الكائن، ومحاولة +الاتصال، ثم تحرّره مجددًا قبل الإرجاع إن فشلت تلك المحاولة بشكل متزامن. + +``` +عرف مقبس: سـندنا[مـقبس_ويب] = مـقبس_ويب.أنشئ("ws://localhost:8060/echo")؛ +إذا مقبس.أهو_عدم() { + // رابط أو بروتوكولات غير صالحة - لم يبدأ الاتصال +} +``` + +
+ +``` +def ws: SrdRef[WebSocket] = WebSocket.create("ws://localhost:8060/echo"); +if ws.isNull() { + // bad URL/protocols - the connection never started +} +``` + +
+ +إرجاع `true`/`سـندنا` غير خالٍ من `أنشئ` (`create`) يعني فقط أن محاولة الاتصال قد بدأت — و هذا **لا** +يعني أن الاتصال أصبح مفتوحًا بعد. انتظر `عند_الفتح` (`onOpen`) لذلك. + +### الأحداث + +#### عند_الفتح (onOpen) + +``` +عرف عند_الفتح: إشـارة[مـقبس_ويب، صـحيح]؛ +``` + +
+ +``` +def onOpen: Signal[WebSocket, Int]; +``` + +
+ +يُطلق بمجرد إنشاء الاتصال. الحمولة غير مستخدمة (`صـحيح`، دائما `0`). + +راجع [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/open_event) لمعرفة متى يُطلق +هذا الحدث بالضبط. + +#### عند_استلام_رسالة (onMessage) + +``` +عرف عند_استلام_رسالة: إشـارة[مـقبس_ويب، حـمولة_رسالة_مقبس]؛ +``` + +
+ +``` +def onMessage: Signal[WebSocket, WsMessageInfo]; +``` + +
+ +يُطلق مرة واحدة لكل رسالة مستلمة. الحمولة هي `حـمولة_رسالة_مقبس` (`WsMessageInfo`)، وتحمل نوع الرسالة +(نصية أو ثنائية) وبياناتها: + +``` +صنف حـمولة_رسالة_مقبس { + عرف البيانات: نـص؛ + عرف ثنائي: ثـنائي؛ +} +``` + +
+ +``` +class WsMessageInfo { + def data: String; + def isBinary: Bool; +} +``` + +
+ +* `البيانات` (`data`): محتوى الرسالة. للرسائل النصية، النص نفسه. للرسائل الثنائية، البايتات الخام + مغلّفة داخل `نـص` (`String`). +* `ثنائي` (`isBinary`): `1` إن أُرسلت الرسالة كبيانات ثنائية، `0` إن أُرسلت كنص. + +راجع [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/message_event) لمعرفة متى يُطلق +هذا الحدث بالضبط. + +#### عند_الخطأ (onError) + +``` +عرف عند_الخطأ: إشـارة[مـقبس_ويب، صـحيح]؛ +``` + +
+ +``` +def onError: Signal[WebSocket, Int]; +``` + +
+ +يُطلق عند حدوث خطأ حقيقي على مستوى الاتصال. الحمولة غير مستخدمة (`صـحيح`، دائما `0`). + +راجع [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/error_event) لمعرفة متى يُطلق +هذا الحدث بالضبط. + +#### عند_الإغلاق (onClose) + +``` +عرف عند_الإغلاق: إشـارة[مـقبس_ويب، حـمولة_إغلاق_مقبس]؛ +``` + +
+ +``` +def onClose: Signal[WebSocket, WsCloseInfo]; +``` + +
+ +يُطلق بمجرد إغلاق الاتصال بشكل كامل، سواء بدأه الطرف المحلي أو الخادم. الحمولة هي `حـمولة_إغلاق_مقبس` +(`WsCloseInfo`): + +``` +صنف حـمولة_إغلاق_مقبس { + عرف الرمز: صـحيح؛ + عرف السبب: نـص؛ + عرف نظيف: ثـنائي؛ +} +``` + +
+ +``` +class WsCloseInfo { + def code: Int; + def reason: String; + def wasClean: Bool; +} +``` + +
+ +* `الرمز` (`code`): رمز إغلاق المقبس. +* `السبب` (`reason`): نص سبب الإغلاق، إن وُجد. +* `نظيف` (`wasClean`): فيما إذا اكتملت مصافحة الإغلاق بشكل نظيف. + +راجع [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close_event) لمعرفة متى يُطلق +هذا الحدث بالضبط. + +## دوال مـقبس_ويب + +#### أرسل_نصا (sendText) + +``` +عملية هذا.أرسل_نصا(بيانات: مؤشر[مـحرف]): مؤشر[مصفوفة[مـحرف]]؛ +``` + +
+ +``` +handler this.sendText(data: ptr[Char]): ptr[array[Char]]; +``` + +
+ +يُرسل رسالة نصية. + +يُرجع مؤشرًا خاليًا إن لم يرمِ المتصفح استثناءً، +أو يُرجع مؤشرًا لرسالة الخطأ إن رُمي استثناء. + +راجع [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send#exceptions) لمعرفة متى +يُرمى الاستثناء الأصلي بالضبط. + +#### أرسل_ثنائيا (sendBinary) + +``` +عملية هذا.أرسل_ثنائيا(بيانات: مؤشر[مـحرف]، طول_البيانات: طـبيعي_متكيف): مؤشر[مصفوفة[مـحرف]]؛ +``` + +
+ +``` +handler this.sendBinary(data: ptr[Char], dataLen: ArchWord): ptr[array[Char]]; +``` + +
+ +يُرسل `طول_البيانات` (`dataLen`) بايتات خامة بدءًا من `بيانات` (`data`). نفس صيغة الإرجاع المستخدمة +في `أرسل_نصا` (`sendText`). + +## أغلق (close) + +``` +عملية هذا.أغلق(): مؤشر[مصفوفة[مـحرف]]؛ +عملية هذا.أغلق(الرمز: صـحيح): مؤشر[مصفوفة[مـحرف]]؛ +عملية هذا.أغلق(الرمز: صـحيح، السبب: مؤشر[مـحرف]): مؤشر[مصفوفة[مـحرف]]؛ +``` + +
+ +``` +handler this.close(): ptr[array[Char]]; +handler this.close(code: Int): ptr[array[Char]]; +handler this.close(code: Int, reason: ptr[Char]): ptr[array[Char]]; +``` + +
+ +يُغلق الاتصال. الصيغة بلا معطيات تستخدم الرمز `1000` (إغلاق عادي) دون سبب؛ بينما تتيح لك الصيغتان +الأخريان تحديد رمز و/أو سبب. + +يُرجع مؤشرًا خاليًا إن لم يرمِ المتصفح استثناءً، +أو يُرجع مؤشرًا لرسالة الخطأ إن رُمي استثناء. + +راجع [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#exceptions) لمعرفة متى +يُرمى الاستثناء الأصلي بالضبط. + +## الحالة (state) + +هذه الدوال تُرجع الخصائص الموجودة في [كائن WebSocket في المتصفح](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket). + +تبقى جميعها تعمل بعد إغلاق الاتصال، تمامًا كما يفعل كائن `WebSocket` الحقيقي — إغلاق الاتصال لا يجعله +يتوقف عن كونه قابلاً للاستعلام. تتوقف عن الصلاحية فقط عند تدمير كائن `مـقبس_ويب` (`WebSocket`) نفسه. + +#### هات_الحالة (getState) + +``` +عملية هذا.هات_الحالة(): صـحيح؛ +``` + +
+ +``` +handler this.getState(): Int; +``` + +
+ +يُرجع خاصية [readyState](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState) من +كائن WebSocket في المتصفح. + +#### هات_الرابط (getUrl) + +``` +عملية هذا.هات_الرابط(): مؤشر[مصفوفة[مـحرف]]؛ +``` + +
+ +``` +handler this.getUrl(): ptr[array[Char]]; +``` + +
+ +يُرجع خاصية [url](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/url) من كائن WebSocket +في المتصفح. + +#### هات_البروتوكول (getProtocol) + +``` +عملية هذا.هات_البروتوكول(): مؤشر[مصفوفة[مـحرف]]؛ +``` + +
+ +``` +handler this.getProtocol(): ptr[array[Char]]; +``` + +
+ +يُرجع خاصية [protocol](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/protocol) من كائن +WebSocket في المتصفح. + +#### هات_الامتدادات (getExtensions) + +``` +عملية هذا.هات_الامتدادات(): مؤشر[مصفوفة[مـحرف]]؛ +``` + +
+ +``` +handler this.getExtensions(): ptr[array[Char]]; +``` + +
+ +يُرجع خاصية [extensions](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/extensions) من +كائن WebSocket في المتصفح. + +#### هات_الكمية_المخزنة (getBufferedAmount) + +``` +عملية هذا.هات_الكمية_المخزنة(): طـبيعي_متكيف؛ +``` + +
+ +``` +handler this.getBufferedAmount(): ArchWord; +``` + +
+ +يُرجع خاصية [bufferedAmount](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/bufferedAmount) +من كائن WebSocket في المتصفح. + +## دورة الحياة (lifetime) + +لا توجد دالة صريحة لـ"قطع الاتصال والتحرير" — تدمير `سـندنا[مـقبس_ويب]` (`SrdRef[WebSocket]`) (سواء +بخروجه من النطاق، أو إعادة تعيينه، أو استدعاء `.release()` عليه) يُغلق الاتصال برمز الحالة `1000` +ويقوم بكل عمليات التنظيف تلقائيًا. + +
diff --git a/Doc/webSocketApi.en.md b/Doc/webSocketApi.en.md new file mode 100644 index 0000000..04cf1b5 --- /dev/null +++ b/Doc/webSocketApi.en.md @@ -0,0 +1,227 @@ +# WebSocket Browser Api + +The browser-side counterpart to the server-side `WsConnection`. + +Read the [MDN WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) docs first +to understand the underlying browser object this class wraps. + +#### construction of the object + +``` +class WebSocket { + def onOpen: Signal[WebSocket, Int]; + def onMessage: Signal[WebSocket, WsMessageInfo]; + def onError: Signal[WebSocket, Int]; + def onClose: Signal[WebSocket, WsCloseInfo]; + + func create(url: ptr[Char], protocols: ptr[Char]): SrdRef[WebSocket]; + func create(url: ptr[Char]): SrdRef[WebSocket]; + + handler this.sendText(data: ptr[Char]): ptr[array[Char]]; + handler this.sendBinary(data: ptr[Char], dataLen: ArchWord): ptr[array[Char]]; + + handler this.close(): ptr[array[Char]]; + handler this.close(code: Int): ptr[array[Char]]; + handler this.close(code: Int, reason: ptr[Char]): ptr[array[Char]]; + + handler this.getState(): Int; + handler this.getUrl(): ptr[array[Char]]; + handler this.getProtocol(): ptr[array[Char]]; + handler this.getExtensions(): ptr[array[Char]]; + handler this.getBufferedAmount(): ArchWord; +} +``` + +#### create a websocket instance + +``` +func create(url: ptr[Char], protocols: ptr[Char]): SrdRef[WebSocket]; +func create(url: ptr[Char]): SrdRef[WebSocket]; +``` + +You get an instance by calling `WebSocket.create(url)` (or `WebSocket.create(url, protocols)` to +offer subprotocols) — this is the *only* way to get one; there is no public constructor. It returns +a `SrdRef[WebSocket]`. + +Always check whether the returned `SrdRef[WebSocket]` `.isNull()` before using it. It will be null if +something went wrong constructing the underlying browser WebSocket object — an invalid URL, a bad +scheme, malformed protocols, a URL containing a fragment, etc. + +See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#exceptions) for the +exact list of exception cases. +This mirrors the real API: if the browser constructor throws, you never get an object back, so this +library never hands you a broken instance either — `create()` builds the object, attempts the +connection, and releases it again before returning if that attempt failed synchronously. + +``` +def ws: SrdRef[WebSocket] = WebSocket.create("ws://localhost:8060/echo"); +if ws.isNull() { + // bad URL/protocols - the connection never started +} +``` + +A `true` return from `create()`/a non-null `SrdRef` only means the connection attempt has started — +it does **not** mean the connection is open yet. Wait for `onOpen` for that. + +### events + +#### onOpen + +``` +def onOpen: Signal[WebSocket, Int]; +``` + +Fires once the connection is established. Payload is unused (`Int`, always `0`). + +See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/open_event) for exactly when +this event fires. + +#### onMessage + +``` +def onMessage: Signal[WebSocket, WsMessageInfo]; +``` + +Fires once per received message. Payload is a `WsMessageInfo`, carrying the message's type (text or +binary) and its data: + +``` +class WsMessageInfo { + def data: String; + def isBinary: Bool; +} +``` + +* `data`: the message content. For text messages, the text itself. For binary messages, the raw bytes wrapped in a `String`. +* `isBinary`: `true` if the message was sent as binary, `false` if sent as text. + +See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/message_event) for exactly when +this event fires. + +#### onError + +``` +def onError: Signal[WebSocket, Int]; +``` + +Fires on a genuine connection-level error. Payload is unused (`Int`, always `0`). + +See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/error_event) for exactly when +this event fires. + +#### onClose + +``` +def onClose: Signal[WebSocket, WsCloseInfo]; +``` + +Fires once the connection is fully closed, whether initiated locally or by the server. Payload is a +`WsCloseInfo`: + +``` +class WsCloseInfo { + def code: Int; + def reason: String; + def wasClean: Bool; +} +``` + +* `code`: the WebSocket close code. +* `reason`: the close reason string, if any. +* `wasClean`: whether the closing handshake completed cleanly. + +See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close_event) for exactly when +this event fires. + +## websocket object methodes + +#### sendText + +``` +handler this.sendText(data: ptr[Char]): ptr[array[Char]]; +``` + +Sends a text message. + +Returns a null pointer if the browser doesn't throw an exception, +or returns a pointer to the error message if an exception is thrown. + +See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/send#exceptions) for exactly +when the underlying exception is raised. + +#### sendBinary + +``` +handler this.sendBinary(data: ptr[Char], dataLen: ArchWord): ptr[array[Char]]; +``` + +Sends `dataLen` raw bytes starting at `data`. Same return convention as `sendText`. + +## close + +``` +handler this.close(): ptr[array[Char]]; +handler this.close(code: Int): ptr[array[Char]]; +handler this.close(code: Int, reason: ptr[Char]): ptr[array[Char]]; +``` + +Closes the connection. The no-argument form uses code `1000` (normal closure) with no reason; the +other overloads let you specify a code and/or reason. + +Returns a null pointer if the browser doesn't throw an exception, +or returns a pointer to the error message if an exception is thrown. + +See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close#exceptions) for exactly +when the underlying exception is raised. + +## state + +these methodes return the properties in the [Websocket browser API object](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket). + +They all keep working after the connection has closed, same as the real `WebSocket` object does — +closing a connection doesn't make it stop being queryable. They only stop being valid once the +`WebSocket` object itself is destroyed. + +#### getState + +``` +handler this.getState(): Int; +``` + +return the [readyState](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState) prop on the Websocket browser object. + +#### getUrl + +``` +handler this.getUrl(): ptr[array[Char]]; +``` +return the [url](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/url) prop on the Websocket browser object. + +#### getProtocol + +``` +handler this.getProtocol(): ptr[array[Char]]; +``` + +return the [protocol](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/protocol) prop on the Websocket browser object. + +#### getExtensions + +``` +handler this.getExtensions(): ptr[array[Char]]; +``` +return the [extensions](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/extensions) prop on the Websocket browser object. + +#### getBufferedAmount + +``` +handler this.getBufferedAmount(): ArchWord; +``` + +return the [bufferedAmount](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/bufferedAmount) prop on the Websocket browser object. + +## lifetime + +There's no explicit "disconnect and free" method — destroying the `SrdRef[WebSocket]` (it going out +of scope, being reassigned, or `.release()` being called on it) closes the connection with status code (1000) and cleans +everything up automatically.