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؛