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.
diff --git a/Doc/ws_endpoints.ar.md b/Doc/ws_endpoints.ar.md
new file mode 100644
index 0000000..372be76
--- /dev/null
+++ b/Doc/ws_endpoints.ar.md
@@ -0,0 +1,312 @@
+# مـنصة_ويب (WebPlatform)
+
+
+
+[[English]](websocketEndpoint.md)
+
+[[رجوع]](README.md)
+
+## إنشاء منافذ المقبس (WebSocket)
+
+يمكنك إنشاء منفذ مقبس بكتابة صنف عادي، وإضافة المبدل `منفذ_مقبس` (`wsEndpoint`) له مع تحديد المسار
+الذي سيستمع عليه، ثم حقن `اتـصال_مقبس` (`WsConnection`) فيه باستخدام المبدل `@حقنة`.
+
+```
+@منفذ_مقبس["/chat"]
+صنف مقبس_الدردشة {
+ @حقنة عرف اتصال_مقبس: اتـصال_مقبس؛
+}
+```
+
+
+
+```
+@wsEndpoint["/chat"]
+class Chatwebsocket {
+ @injection def WsConnection: WsConnection;
+}
+```
+
+
+
+الآن أصبح لديك منفذ مقبس يستمع على `/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)
+
+يمنحك `اتـصال_مقبس` أربعة معالجات يمكنك تخصيصها للتفاعل مع دورة حياة الاتصال:
+
+* `عند_الاتصال` (`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..cc7cbef
--- /dev/null
+++ b/Doc/ws_endpoints.en.md
@@ -0,0 +1,193 @@
+# 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.
+
+## 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:
+
+* `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);
+```
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/Utils/WebSocket.alusus b/WebPlatform/Utils/WebSocket.alusus
new file mode 100644
index 0000000..80e787c
--- /dev/null
+++ b/WebPlatform/Utils/WebSocket.alusus
@@ -0,0 +1,169 @@
+/*
+ * 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];
+
+ // 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;
+ }
+ func create(url: ptr[Char]): SrdRef[WebSocket] {
+ return WebSocket.create(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 {
+ 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: 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 {
+ 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);
+ // 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;
+ }
+ });
+ 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;
+ }
+
+ // 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): ptr[array[Char]] {
+ return _sendWebSocketBinary(this.socketId, data, dataLen);
+ }
+
+ handler this.close(): ptr[array[Char]] {
+ return this.close(1000, "");
+ }
+
+ handler this.close(code: Int): ptr[array[Char]] {
+ return this.close(code, "");
+ }
+
+ handler this.close(code: Int, reason: ptr[Char]): ptr[array[Char]] {
+ return _closeWebSocket(this.socketId, code, reason);
+ }
+
+ handler this.getState(): Int {
+ return getWebSocketState(this.socketId);
+ }
+
+ handler this.getUrl(): ptr[array[Char]] {
+ return getWebSocketUrl(this.socketId);
+ }
+
+ handler this.getProtocol(): ptr[array[Char]] {
+ return _getWebSocketProtocol(this.socketId);
+ }
+
+ handler this.getExtensions(): ptr[array[Char]] {
+ return _getWebSocketExtensions(this.socketId);
+ }
+
+ handler this.getBufferedAmount(): ArchWord {
+ return _getWebSocketBufferedAmount(this.socketId);
+ }
+ }
+}
diff --git a/WebPlatform/WsConnection.alusus b/WebPlatform/WsConnection.alusus
new file mode 100644
index 0000000..2dce50d
--- /dev/null
+++ b/WebPlatform/WsConnection.alusus
@@ -0,0 +1,205 @@
+@merge module WebPlatform {
+ class WsStatus {
+ setupStringEnum[];
+ enumStringValue[CONNECTING, "connecting"];
+ 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 {
+ 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.getStatus(): WsStatus {
+ return this.status;
+ }
+
+ handler this.setMaxMessageSize(size: ArchInt) {
+ this.maxMessageSize = size;
+ this.fragmentBuffer.bufferGrowSize = (this.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;
+ }
+
+ def payload : array[Char, 125];
+
+ payload(0) = (statusCode >> 8) & 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) <= 0 {
+ this.status = WsStatus.CLOSED;
+ }
+ }
+
+ // 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;
+ }
+ }
+
+ handler this.onConnect(connection : ptr[Http.Connection]): Int as_ptr {
+ return 0;
+ }
+
+ handler this.onReady() : Void as_ptr {
+ }
+
+ 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 data_Len : 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 (data_Len > this.getMaxMessageSize()) {
+ this.close(1009, "Message too big");
+ return 0;
+ }
+
+ this.fragmentBuffer.append(data, data_Len);
+
+ if (fin) {
+ def isBinary : Bool = this.fragmentOpcode == 2;
+ this.onData(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() + data_Len > this.getMaxMessageSize()) {
+ this.close(1009, "Message too big");
+ return 0;
+ }
+
+ this.fragmentBuffer.append(data, data_Len);
+
+ if (fin) {
+ def isBinary : Bool = this.fragmentOpcode == 2;
+ this.onData(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 {
+ 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.onData(data: ref[String], isBinary: Bool) : Void as_ptr {
+ }
+
+ handler this.onClose() as_ptr {
+ }
+ }
+}
diff --git a/WebPlatform/browser_api.alusus b/WebPlatform/browser_api.alusus
index 4d6ce8d..265b125 100644
--- a/WebPlatform/browser_api.alusus
+++ b/WebPlatform/browser_api.alusus
@@ -66,6 +66,18 @@
@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]): 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]): 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/WebPlatform/server.alusus b/WebPlatform/server.alusus
index 016762d..d34681c 100644
--- a/WebPlatform/server.alusus
+++ b/WebPlatform/server.alusus
@@ -63,6 +63,7 @@
for i = 0, i < elements.getLength(), ++i {
def uriParams: Array[String] = getModifierParams(elements(i), "uiEndpoint", "منفذ_مرئي");
if uriParams.getLength() < 1 {
+ // TODO: Raise build notice instead
System.fail(1, "Invalid @uiEndpoint params");
}
@@ -217,6 +218,7 @@
for i = 0, i < elements.getLength(), ++i {
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));
@@ -231,18 +233,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]] = findFunctionElements(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 {
+ 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 +272,67 @@
}
}
+ 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)
+ or wsEndpointParam.getLength() != 1 {
+ // TODO: Raise build notice instead.
+ 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)))
+ );
+ }
+ }
+
+ 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] = getModifierParams(webSocketClasses(i) , "wsEndpoint", "منفذ_مقبس");
+
+ if (params.getLength() != 1) {
+ System.fail(1 , "where is the endpoint");
+ }
+
+ Spp.astMgr.insertAst(
+ ast {
+ Http.setWebSocketHandler(
+ httpContext,
+ "{{wsEndpointUri}}",
+ 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"), constructElementFullReference(webSocketClasses(i)))
+ );
+ }
+ }
+
// Querying Functions
func getAssetsRoutesFromModules (modulesRef: ref[Core.Basic.TiObject]): Array[StaticRoute] {
@@ -326,6 +383,33 @@
func startServer [modulesRef: ast_ref = Root] (
mainAssetsPath: String, uiEndpointsPath: String, options: Array[CharsPtr], useSrcAssetsPath: Bool
): ptr[ServerSession] {
+ // 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");
+ }
+
+ // 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
+ // 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);
@@ -351,6 +435,15 @@
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 {
+ Spp.buildMgr.raiseBuildNotice("SPPA1002", 1, modulesRef~ast);
+ }
+ def i: Int;
+ for i = 0, i < modules.getLength(), ++i generateWebSocketRegistrations(modules(i));
+ };
+
return session;
}
@@ -414,7 +507,10 @@
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") {
@@ -463,6 +559,47 @@
return 1;
}
+ func wsConnectCallback [WsConnClass: type] (connection: ptr[Http.Connection], userData: ptr[Void]): Int {
+ def ws: SrdRef[WsConnClass];
+ 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~cast[ptr[RefCounter]]);
+
+ return ws.onConnect(ws.connection);
+ }
+
+ 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, rc~cnt.managedObj~cast[ptr[WsConnClass]]~cnt);
+ 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, 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, rc~cnt.managedObj~cast[ptr[WsConnClass]]~cnt);
+ 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);
@@ -474,31 +611,60 @@
// 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;
+ }
+
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);
};
}
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 translations: Map[String, String];
+ translations.set(String(arKwd), String(enKwd));
+ def modifier: ref[Core.Basic.TiObject](Spp.astMgr.findModifierForElement(element, 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(
+ ast { elementType == "function" },
+ modulesRef,
+ Spp.SeekerFlags.SKIP_OWNERS | Spp.SeekerFlags.SKIP_USES | Spp.SeekerFlags.SKIP_CHILDREN,
+ enName,
+ translations
+ );
+ }
+
+ 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(
+ ast { elementType == "type" },
+ modulesRef,
+ Spp.SeekerFlags.SKIP_OWNERS | Spp.SeekerFlags.SKIP_USES | Spp.SeekerFlags.SKIP_CHILDREN,
+ enName,
+ translations
+ );
+ }
+
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] {
diff --git a/api.js b/api.js
index 71f90ea..b6468c0 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,124 @@ 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;
+ }
+
+ 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
+ });
+ };
+
+ return socketId;
+}
+
+wasmApi.sendWebSocketMessage = (socketId, data) => {
+ const ws = webSockets[socketId];
+ try {
+ ws.send(toJsString(data));
+ return 0;
+ } catch (err) {
+ return toWasmString(err.message);
+ }
+};
+
+wasmApi.sendWebSocketBinary = (socketId, dataPtr, dataLen) => {
+ const ws = webSockets[socketId];
+ try {
+ ws.send(new Uint8Array(wasmMemory.buffer, dataPtr, dataLen));
+ return 0;
+ } catch (err) {
+ return toWasmString(err.message);
+ }
+};
+
+wasmApi.copyWebSocketBinaryData = (dataId, destPtr) => {
+ const bytes = webSocketBinaryData[dataId];
+ new Uint8Array(wasmMemory.buffer, destPtr, bytes.length).set(bytes);
+ delete webSocketBinaryData[dataId];
+};
+
+wasmApi.closeWebSocket = (socketId, code, reason) => {
+ const ws = webSockets[socketId];
+ try {
+ ws.close(code, toJsString(reason));
+ return 0;
+ } catch (err) {
+ return toWasmString(err.message);
+ }
+};
+
+wasmApi.getWebSocketState = (socketId) => {
+ const ws = webSockets[socketId];
+ return ws.readyState;
+};
+
+wasmApi.getWebSocketUrl = (socketId) => {
+ const ws = webSockets[socketId];
+ return toWasmString(ws.url);
+};
+
+wasmApi.getWebSocketProtocol = (socketId) => {
+ const ws = webSockets[socketId];
+ return toWasmString(ws.protocol);
+};
+
+wasmApi.getWebSocketExtensions = (socketId) => {
+ const ws = webSockets[socketId];
+ return toWasmString(ws.extensions);
+};
+
+wasmApi.getWebSocketBufferedAmount = (socketId) => {
+ const ws = webSockets[socketId];
+ return ws.bufferedAmount;
+};
+
+wasmApi.deleteWebSocket = (socketId) => {
+ delete webSockets[socketId];
+};
+
// Resource Management
wasmApi.loadImage = (url, cbId) => {
@@ -1095,6 +1217,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,
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؛