-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwebsocket.mbt
More file actions
241 lines (221 loc) · 6.99 KB
/
Copy pathwebsocket.mbt
File metadata and controls
241 lines (221 loc) · 6.99 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
// WebSocket routes (← FastAPI's `@app.websocket(path)`). A handler talks to a
// `WebSocket` value — accept, receive, send, close — whose actions are recorded
// as `@moonasgi.Event`s (`WebSocketAccept` / `WebSocketSendText` / … / the WS
// half of the moonasgi SEAM). The handler is a synchronous core, so it runs
// identically in a test (drive an in-memory frame queue with `drive_websocket`)
// and under a real server. `App::to_asgi` provides the async serving shell.
///|
/// One inbound WebSocket message: a text frame or a binary frame.
pub(all) enum WsMessage {
WsText(String)
WsBinary(Bytes)
} derive(Eq)
///|
/// The handler's view of a WebSocket connection. It reads client frames off an
/// inbound queue and records its own actions (accept / send / close) into an
/// outbound event log the transport replays. `params` are the matched `:name`
/// path segments, as with an HTTP `Context`.
pub struct WebSocket {
inbox : Array[WsMessage]
mut cursor : Int
params : Map[String, String]
subprotocols : Array[String]
mut accepted : Bool
mut closed : Bool
outbox : Array[@moonasgi.Event]
}
///|
/// A WebSocket route handler: given the connection, drive the exchange. Usually
/// `accept`, then a `receive` loop, then `close`.
pub type WsHandler = (WebSocket) -> Unit
///|
struct WsRoute {
path : String
handler : WsHandler
}
///|
/// Build a connection over a pre-supplied inbound queue — the shape a test or
/// the serving shell hands the handler.
fn WebSocket::new(
inbox : Array[WsMessage],
params : Map[String, String],
subprotocols : Array[String],
) -> WebSocket {
{
inbox,
cursor: 0,
params,
subprotocols,
accepted: false,
closed: false,
outbox: [],
}
}
///|
/// Look up a matched path parameter by name.
pub fn WebSocket::param(self : WebSocket, name : String) -> String? {
self.params.get(name)
}
///|
/// The subprotocols the client offered (the `Sec-WebSocket-Protocol` list).
pub fn WebSocket::offered_subprotocols(self : WebSocket) -> Array[String] {
self.subprotocols
}
///|
/// Accept the handshake (← `await websocket.accept()`), optionally selecting a
/// `subprotocol` and adding response `headers`. Idempotent: a second call is a
/// no-op, so accept-once handlers stay simple.
pub fn WebSocket::accept(
self : WebSocket,
subprotocol? : String,
headers? : Array[(String, String)] = [],
) -> Unit {
if self.accepted {
return
}
self.accepted = true
self.outbox.push(@moonasgi.Event::WebSocketAccept(subprotocol~, headers~))
}
///|
/// Pull the next client frame, `None` once the client has sent them all (the
/// disconnect). The `receive` a handler loops on.
pub fn WebSocket::receive(self : WebSocket) -> WsMessage? {
if self.cursor >= self.inbox.length() {
return None
}
let m = self.inbox[self.cursor]
self.cursor = self.cursor + 1
Some(m)
}
///|
/// The next client frame as text: `Some(s)` for a text frame, `None` on a
/// binary frame or the disconnect (← `await websocket.receive_text()`).
pub fn WebSocket::receive_text(self : WebSocket) -> String? {
match self.receive() {
Some(WsText(s)) => Some(s)
_ => None
}
}
///|
/// The next client frame as bytes: `Some(b)` for a binary frame, `None` on a
/// text frame or the disconnect.
pub fn WebSocket::receive_bytes(self : WebSocket) -> Bytes? {
match self.receive() {
Some(WsBinary(b)) => Some(b)
_ => None
}
}
///|
/// Send a text frame to the client (← `await websocket.send_text(...)`).
pub fn WebSocket::send_text(self : WebSocket, text : String) -> Unit {
self.outbox.push(@moonasgi.Event::WebSocketSendText(text))
}
///|
/// Send a binary frame to the client.
pub fn WebSocket::send_bytes(self : WebSocket, bytes : Bytes) -> Unit {
self.outbox.push(@moonasgi.Event::WebSocketSendBytes(bytes))
}
///|
/// Close the connection with a status `code` (default `1000`, normal closure)
/// and `reason`. Idempotent.
pub fn WebSocket::close(
self : WebSocket,
code? : Int = 1000,
reason? : String = "",
) -> Unit {
if self.closed {
return
}
self.closed = true
self.outbox.push(@moonasgi.Event::WebSocketClose(code~, reason~))
}
///|
/// The events the handler emitted, in order — the transcript a test asserts on.
pub fn WebSocket::sent(self : WebSocket) -> Array[@moonasgi.Event] {
self.outbox
}
///|
/// Register a WebSocket route (← FastAPI's `@app.websocket(path)`). The path
/// matches with the same `:name` segment rules as HTTP routes.
pub fn App::websocket(self : App, path : String, handler : WsHandler) -> Unit {
self.ws_routes.push({ path, handler, })
}
///|
/// Match a WebSocket path against the registered routes, returning the handler
/// and the extracted path parameters.
fn App::match_ws(
self : App,
path : String,
) -> (WsHandler, Map[String, String])? {
for route in self.ws_routes {
match match_path(route.path, path) {
Some(params) => return Some((route.handler, params))
None => ()
}
}
None
}
///|
/// Run a WebSocket handler against an in-memory frame queue and return the
/// events it emitted — the synchronous test driver (the WS half of a
/// `TestClient`). Feed the client's frames as `inbound`; get back the handler's
/// accept / send / close sequence.
pub fn drive_websocket(
handler : WsHandler,
inbound : Array[WsMessage],
params? : Map[String, String] = Map([]),
subprotocols? : Array[String] = [],
) -> Array[@moonasgi.Event] {
let sock = WebSocket::new(inbound, params, subprotocols)
handler(sock)
sock.outbox
}
///|
/// The async serving shell for a WebSocket scope, driven by `App::to_asgi`. It
/// buffers the client's inbound frames off the SEAM, runs the handler's sync
/// core, then emits its accept / send / close events.
///
/// Because the sync core cannot suspend on the async transport (MoonBit's async
/// wall — see the README design note), it sees the client's whole frame
/// sequence before it runs rather than interleaving live. Message content and
/// order are preserved, which is exact for echo, broadcast, and request-reply
/// handlers; live per-frame duplex is the documented boundary.
async fn App::serve_websocket(
self : App,
scope : @moonasgi.WebSocketScope,
receive : @moonasgi.Receive,
send : @moonasgi.Send,
) -> Unit {
let (handler, params) = match self.match_ws(scope.path) {
Some(pair) => pair
None => {
send(@moonasgi.Event::WebSocketClose(code=1000, reason="no route"))
return
}
}
// Consume the connect, then drain frames until the client disconnects.
let _ = receive()
let inbox : Array[WsMessage] = []
let mut open = true
while open {
match receive() {
WebSocketReceive(text~, bytes~) =>
match text {
Some(t) => inbox.push(WsText(t))
None =>
match bytes {
Some(b) => inbox.push(WsBinary(b))
None => ()
}
}
_ => open = false
}
}
let sock = WebSocket::new(inbox, params, scope.subprotocols)
handler(sock)
for ev in sock.outbox {
send(ev)
}
}
///|
pub extend WsMessage with Eq::{not_equal, equal}