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
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
//! One client-side WebSocket interface, whatever is underneath.
//!
//! `plaza_session` covers the server and is tokio/actix by construction, so a
//! client cannot use it (a browser client least of all). This crate covers the
//! client side: the socket a *client* holds, with the same shape on a desktop,
//! in a browser and in-process.
//!
//! # Built for a frame loop
//!
//! [`Socket::poll`] is non-blocking and drains into a caller-owned buffer. That
//! choice is made for macroquad-style applications, which have a synchronous
//! `loop { ...; next_frame().await }` and nowhere to put a future. An
//! `async fn recv()` would be the natural Rust API and would be unusable there.
//! Reusing the buffer also keeps a per-frame call allocation-free, matching how
//! `plaza_server_utils` hands back its results.
//!
//! ```no_run
//! # use plaza_ws::{Socket, Event};
//! # fn demo(socket: &mut impl Socket) {
//! let mut events = Vec::new();
//! // once per frame
//! socket.poll(&mut events);
//! for event in events.drain(..) {
//! match event {
//! Event::Open => println!("connected"),
//! Event::Message(bytes) => println!("{} bytes", bytes.len()),
//! Event::Text(text) => println!("{text}"),
//! Event::Closed(reason) => println!("gone: {reason:?}"),
//! }
//! }
//! # }
//! ```
//!
//! # Backends
//!
//! Each is a feature. They can be combined: a native host that also plays
//! enables `native` *and* `loopback` and talks to both over the same trait.
//!
//! | feature | where | underneath |
//! |---|---|---|
//! | `loopback` | anywhere | in-process channels, no dependencies |
//! | `native` | desktop | `tungstenite` on a worker thread |
//! | `miniquad` | browser, under macroquad | our own JS, registered as a miniquad plugin |
//!
//! ## The browser backend
//!
//! The constraint comes from the *host page's loader* rather than from the
//! browser.
//!
//! `web-sys` (and with it `gloo-net` and `tokio-tungstenite-wasm`) needs
//! `wasm-bindgen`, which rewrites the module with `wasm-bindgen-cli` and ships
//! its own JS to instantiate it. miniquad's `mq_js_bundle.js` builds its own
//! import object, lets plugins extend it and instantiates the raw module
//! itself. Both want to own instantiation, so under macroquad the wasm-bindgen
//! route does not work. It also fails silently: miniquad stubs out imports
//! nothing provides, so such a build loads without error and then does nothing.
//!
//! So the `miniquad` backend is a few `extern "C"` declarations against our own
//! JS plugin (`js/plaza_ws.js`), which needs no crate at all. The two crates
//! that *do* use miniquad's plugin mechanism, `sapp-jsutils` and `quad-net`, are
//! barely maintained and the mechanism is small enough not to need them.
//!
//! An application that is **not** built on macroquad has the opposite problem
//! and wants the wasm-bindgen route. It would be a fourth backend
//! (`tokio-tungstenite-wasm` behind a `web` feature), left out until something
//! needs it so it is not shipped untested.
use fmt;
pub use ;
/// Something that arrived, in order.
/// Why a socket ended.
/// Where a socket is in its lifecycle. Monotonic: a socket never returns to an
/// earlier state, so [`Closed`](State::Closed) is terminal and reconnecting
/// means a new socket.
/// A client-side WebSocket.
///
/// Deliberately small. Anything an application can do itself (reconnection
/// policy, backoff, heartbeats, framing of its own messages) is left to it,
/// because those are the application's decisions and the socket only moves
/// bytes.
/// Sending a value as JSON text, so call sites are not full of
/// `serde_json::to_string`.
///
/// An extension trait rather than a method on [`Socket`], because a generic
/// method cannot be called through a trait object, and holding the socket as
/// `Box<dyn Socket>` is exactly what an application does when the transport is
/// chosen by feature flag. Implemented for every socket, sized or not.
///
/// Text rather than binary, deliberately. A WebSocket text frame arrives in a
/// browser as a string that `JSON.parse` accepts directly, while a binary frame
/// arrives as a `Blob` or `ArrayBuffer` that a JS client has to decode itself,
/// having first remembered to set `binaryType`.
///
/// Send a **bare message rather than an envelope**. A server attaches who a message
/// came from rather than trusting the client to say, because a client that
/// could name itself could name somebody else.
/// Connects using whichever real transport this build has for its target.
///
/// The choice is never ambiguous: [`native`] exists only off wasm and
/// `miniquad` only on it, so a build that enables both features (the normal
/// shape for an application shipping a desktop and a browser client from one
/// crate) still has exactly one real backend per target. [`loopback`] is never
/// chosen here because it connects to a peer rather than to a URL.
/// [`connect`], boxed: the form an application holds when the backend is
/// decided by the build rather than written at the call site.
///
/// Unlike [`connect`], this exists in every build. A build with no real backend
/// gets a runtime [`WsError::Connect`] instead of a compile error, because such
/// a build is legitimate (an offline teaching build still compiles its connect
/// path) and every application ends up writing this same fallback arm itself.