@tishlang/tish 1.0.7 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +43 -0
- package/LICENSE +13 -0
- package/README.md +66 -0
- package/crates/js_to_tish/Cargo.toml +9 -0
- package/crates/js_to_tish/README.md +18 -0
- package/crates/js_to_tish/src/error.rs +61 -0
- package/crates/js_to_tish/src/lib.rs +11 -0
- package/crates/js_to_tish/src/span_util.rs +35 -0
- package/crates/js_to_tish/src/transform/expr.rs +608 -0
- package/crates/js_to_tish/src/transform/stmt.rs +474 -0
- package/crates/js_to_tish/src/transform.rs +60 -0
- package/crates/tish/Cargo.toml +44 -0
- package/crates/tish/src/main.rs +585 -0
- package/crates/tish/src/repl_completion.rs +200 -0
- package/crates/tish/tests/integration_test.rs +726 -0
- package/crates/tish_ast/Cargo.toml +7 -0
- package/crates/tish_ast/src/ast.rs +494 -0
- package/crates/tish_ast/src/lib.rs +5 -0
- package/crates/tish_build_utils/Cargo.toml +5 -0
- package/crates/tish_build_utils/src/lib.rs +175 -0
- package/crates/tish_builtins/Cargo.toml +12 -0
- package/crates/tish_builtins/src/array.rs +410 -0
- package/crates/tish_builtins/src/globals.rs +197 -0
- package/crates/tish_builtins/src/helpers.rs +38 -0
- package/crates/tish_builtins/src/lib.rs +14 -0
- package/crates/tish_builtins/src/math.rs +80 -0
- package/crates/tish_builtins/src/object.rs +36 -0
- package/crates/tish_builtins/src/string.rs +253 -0
- package/crates/tish_bytecode/Cargo.toml +15 -0
- package/crates/tish_bytecode/src/chunk.rs +97 -0
- package/crates/tish_bytecode/src/compiler.rs +1361 -0
- package/crates/tish_bytecode/src/encoding.rs +100 -0
- package/crates/tish_bytecode/src/lib.rs +19 -0
- package/crates/tish_bytecode/src/opcode.rs +110 -0
- package/crates/tish_bytecode/src/peephole.rs +159 -0
- package/crates/tish_bytecode/src/serialize.rs +163 -0
- package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
- package/crates/tish_bytecode/tests/shortcircuit.rs +49 -0
- package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
- package/crates/tish_compile/Cargo.toml +21 -0
- package/crates/tish_compile/src/codegen.rs +3316 -0
- package/crates/tish_compile/src/lib.rs +71 -0
- package/crates/tish_compile/src/resolve.rs +631 -0
- package/crates/tish_compile/src/types.rs +304 -0
- package/crates/tish_compile_js/Cargo.toml +16 -0
- package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
- package/crates/tish_compile_js/src/codegen.rs +794 -0
- package/crates/tish_compile_js/src/error.rs +20 -0
- package/crates/tish_compile_js/src/js_intrinsics.rs +82 -0
- package/crates/tish_compile_js/src/lib.rs +27 -0
- package/crates/tish_compile_js/src/tests_jsx.rs +32 -0
- package/crates/tish_compiler_wasm/Cargo.toml +19 -0
- package/crates/tish_compiler_wasm/src/lib.rs +55 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +462 -0
- package/crates/tish_core/Cargo.toml +11 -0
- package/crates/tish_core/src/console_style.rs +128 -0
- package/crates/tish_core/src/json.rs +327 -0
- package/crates/tish_core/src/lib.rs +15 -0
- package/crates/tish_core/src/macros.rs +37 -0
- package/crates/tish_core/src/uri.rs +115 -0
- package/crates/tish_core/src/value.rs +376 -0
- package/crates/tish_cranelift/Cargo.toml +17 -0
- package/crates/tish_cranelift/src/lib.rs +41 -0
- package/crates/tish_cranelift/src/link.rs +120 -0
- package/crates/tish_cranelift/src/lower.rs +77 -0
- package/crates/tish_cranelift_runtime/Cargo.toml +19 -0
- package/crates/tish_cranelift_runtime/src/lib.rs +43 -0
- package/crates/tish_eval/Cargo.toml +26 -0
- package/crates/tish_eval/src/eval.rs +3205 -0
- package/crates/tish_eval/src/http.rs +122 -0
- package/crates/tish_eval/src/lib.rs +59 -0
- package/crates/tish_eval/src/natives.rs +301 -0
- package/crates/tish_eval/src/promise.rs +173 -0
- package/crates/tish_eval/src/regex.rs +298 -0
- package/crates/tish_eval/src/timers.rs +111 -0
- package/crates/tish_eval/src/value.rs +224 -0
- package/crates/tish_eval/src/value_convert.rs +85 -0
- package/crates/tish_fmt/Cargo.toml +16 -0
- package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
- package/crates/tish_fmt/src/lib.rs +884 -0
- package/crates/tish_jsx_web/Cargo.toml +7 -0
- package/crates/tish_jsx_web/README.md +18 -0
- package/crates/tish_jsx_web/src/lib.rs +157 -0
- package/crates/tish_jsx_web/vendor/Lattish.tish +347 -0
- package/crates/tish_lexer/Cargo.toml +7 -0
- package/crates/tish_lexer/src/lib.rs +430 -0
- package/crates/tish_lexer/src/token.rs +155 -0
- package/crates/tish_lint/Cargo.toml +17 -0
- package/crates/tish_lint/src/bin/tish-lint.rs +77 -0
- package/crates/tish_lint/src/lib.rs +278 -0
- package/crates/tish_llvm/Cargo.toml +11 -0
- package/crates/tish_llvm/src/lib.rs +106 -0
- package/crates/tish_lsp/Cargo.toml +22 -0
- package/crates/tish_lsp/README.md +26 -0
- package/crates/tish_lsp/src/main.rs +615 -0
- package/crates/tish_native/Cargo.toml +14 -0
- package/crates/tish_native/src/build.rs +102 -0
- package/crates/tish_native/src/lib.rs +237 -0
- package/crates/tish_opt/Cargo.toml +11 -0
- package/crates/tish_opt/src/lib.rs +896 -0
- package/crates/tish_parser/Cargo.toml +9 -0
- package/crates/tish_parser/src/lib.rs +123 -0
- package/crates/tish_parser/src/parser.rs +1714 -0
- package/crates/tish_runtime/Cargo.toml +26 -0
- package/crates/tish_runtime/src/http.rs +308 -0
- package/crates/tish_runtime/src/http_fetch.rs +453 -0
- package/crates/tish_runtime/src/lib.rs +1004 -0
- package/crates/tish_runtime/src/native_promise.rs +26 -0
- package/crates/tish_runtime/src/promise.rs +77 -0
- package/crates/tish_runtime/src/promise_io.rs +41 -0
- package/crates/tish_runtime/src/timers.rs +125 -0
- package/crates/tish_runtime/src/ws.rs +725 -0
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +99 -0
- package/crates/tish_vm/Cargo.toml +31 -0
- package/crates/tish_vm/src/lib.rs +39 -0
- package/crates/tish_vm/src/vm.rs +1399 -0
- package/crates/tish_wasm/Cargo.toml +13 -0
- package/crates/tish_wasm/src/lib.rs +358 -0
- package/crates/tish_wasm_runtime/Cargo.toml +25 -0
- package/crates/tish_wasm_runtime/src/lib.rs +36 -0
- package/justfile +260 -0
- package/package.json +8 -3
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- package/platform/win32-x64/tish.exe +0 -0
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
//! WebSocket module for Tish (tish:ws).
|
|
2
|
+
//!
|
|
3
|
+
//! Node.js `ws`-compatible API:
|
|
4
|
+
//! - **Server**: `Server({ port })` — has `clients` (array), `on('connection', fn)`, `listen()`, `acceptTimeout(server, ms)`
|
|
5
|
+
//! - **Connection**: `send(data)`, `close()`, `readyState` (1=OPEN), `receive()` / `receiveTimeout(ms)`
|
|
6
|
+
//! - **Broadcast** (Node pattern): `server.clients.forEach(ws => ws.send(data))` or iterate room conns and `wsSend(ws, data)` (same as `ws.send(data)`)
|
|
7
|
+
|
|
8
|
+
use std::cell::RefCell;
|
|
9
|
+
use std::collections::HashMap;
|
|
10
|
+
use std::rc::Rc;
|
|
11
|
+
use std::sync::atomic::{AtomicU32, Ordering};
|
|
12
|
+
use std::sync::mpsc;
|
|
13
|
+
use std::sync::{Arc, Mutex};
|
|
14
|
+
use std::time::{Duration, Instant};
|
|
15
|
+
|
|
16
|
+
use futures_util::{SinkExt, StreamExt};
|
|
17
|
+
use lazy_static::lazy_static;
|
|
18
|
+
use tish_core::Value;
|
|
19
|
+
use tokio::sync::mpsc as tokio_mpsc;
|
|
20
|
+
use tokio::runtime::Runtime;
|
|
21
|
+
|
|
22
|
+
thread_local! {
|
|
23
|
+
/// Multi-thread runtime so `tokio::spawn` I/O tasks keep running after `block_on` returns.
|
|
24
|
+
static WS_CLIENT_RT: std::cell::RefCell<Option<Runtime>> = const { std::cell::RefCell::new(None) };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fn with_ws_client_rt<F, R>(f: F) -> R
|
|
28
|
+
where
|
|
29
|
+
F: FnOnce(&Runtime) -> R,
|
|
30
|
+
{
|
|
31
|
+
WS_CLIENT_RT.with(|cell| {
|
|
32
|
+
let mut b = cell.borrow_mut();
|
|
33
|
+
if b.is_none() {
|
|
34
|
+
*b = Some(
|
|
35
|
+
tokio::runtime::Builder::new_multi_thread()
|
|
36
|
+
.worker_threads(2)
|
|
37
|
+
.enable_all()
|
|
38
|
+
.build()
|
|
39
|
+
.expect("ws client tokio runtime"),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
f(b.as_ref().expect("ws runtime"))
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static NEXT_CONN_ID: AtomicU32 = AtomicU32::new(1);
|
|
47
|
+
static NEXT_SERVER_HANDLE: AtomicU32 = AtomicU32::new(1);
|
|
48
|
+
|
|
49
|
+
fn next_conn_id() -> u32 {
|
|
50
|
+
NEXT_CONN_ID.fetch_add(1, Ordering::SeqCst)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fn next_server_handle() -> u32 {
|
|
54
|
+
NEXT_SERVER_HANDLE.fetch_add(1, Ordering::SeqCst)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
struct ConnState {
|
|
58
|
+
send_tx: tokio_mpsc::UnboundedSender<String>,
|
|
59
|
+
recv_rx: mpsc::Receiver<String>,
|
|
60
|
+
#[allow(dead_code)]
|
|
61
|
+
open: bool,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
lazy_static! {
|
|
65
|
+
static ref CONNS: Mutex<HashMap<u32, ConnState>> = Mutex::new(HashMap::new());
|
|
66
|
+
static ref SERVER_RECV: Mutex<HashMap<u32, mpsc::Receiver<u32>>> = Mutex::new(HashMap::new());
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fn register(send_tx: tokio_mpsc::UnboundedSender<String>, recv_rx: mpsc::Receiver<String>) -> u32 {
|
|
70
|
+
let id = next_conn_id();
|
|
71
|
+
CONNS.lock().unwrap().insert(
|
|
72
|
+
id,
|
|
73
|
+
ConnState {
|
|
74
|
+
send_tx,
|
|
75
|
+
recv_rx,
|
|
76
|
+
open: true,
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
id
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fn unregister(id: u32) {
|
|
83
|
+
CONNS.lock().unwrap().remove(&id);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fn conn_send(id: u32, data: String) -> bool {
|
|
87
|
+
let guard = match CONNS.lock() {
|
|
88
|
+
Ok(g) => g,
|
|
89
|
+
Err(_) => return false,
|
|
90
|
+
};
|
|
91
|
+
let state = match guard.get(&id) {
|
|
92
|
+
Some(s) if s.open => s,
|
|
93
|
+
_ => return false,
|
|
94
|
+
};
|
|
95
|
+
state.send_tx.send(data).is_ok()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// Default timeout for receive() so the main thread blocks and keeps the process/runtime alive.
|
|
99
|
+
const RECV_DEFAULT_TIMEOUT_MS: u64 = 2000;
|
|
100
|
+
|
|
101
|
+
fn conn_receive(id: u32) -> Option<String> {
|
|
102
|
+
conn_receive_timeout(id, RECV_DEFAULT_TIMEOUT_MS)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// Block for up to timeout_ms; returns Some(msg) or None on timeout/disconnect.
|
|
106
|
+
/// Uses try_recv in a loop to avoid holding CONNS lock while blocking (prevents deadlock
|
|
107
|
+
/// when connection closes and tokio task needs to unregister).
|
|
108
|
+
fn conn_receive_timeout(id: u32, timeout_ms: u64) -> Option<String> {
|
|
109
|
+
let timeout_ms = timeout_ms.min(3600_000);
|
|
110
|
+
let deadline = Instant::now() + Duration::from_millis(timeout_ms);
|
|
111
|
+
let poll_interval = Duration::from_millis(50);
|
|
112
|
+
loop {
|
|
113
|
+
let result = {
|
|
114
|
+
let guard = match CONNS.lock() {
|
|
115
|
+
Ok(g) => g,
|
|
116
|
+
Err(_) => return None,
|
|
117
|
+
};
|
|
118
|
+
if !guard.contains_key(&id) {
|
|
119
|
+
drop(guard);
|
|
120
|
+
std::thread::sleep(Duration::from_millis(50));
|
|
121
|
+
return None;
|
|
122
|
+
}
|
|
123
|
+
guard.get(&id).unwrap().recv_rx.try_recv()
|
|
124
|
+
};
|
|
125
|
+
match result {
|
|
126
|
+
Ok(s) => return Some(s),
|
|
127
|
+
Err(mpsc::TryRecvError::Disconnected) => return None,
|
|
128
|
+
Err(mpsc::TryRecvError::Empty) => {
|
|
129
|
+
if Instant::now() >= deadline {
|
|
130
|
+
return None;
|
|
131
|
+
}
|
|
132
|
+
crate::timers::sleep_with_drain(poll_interval.as_millis() as u64);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// Native send: avoids method-call path. Takes conn object (with _id) and string data.
|
|
139
|
+
pub fn ws_send_native(conn: &Value, data: &str) -> bool {
|
|
140
|
+
let id = conn_id_from_value(conn);
|
|
141
|
+
match id {
|
|
142
|
+
Some(id) => conn_send(id, data.to_string()),
|
|
143
|
+
None => false,
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/// Extract connection id from conn object { _id, send, ... } or wrapper { ws: conn, ... }.
|
|
148
|
+
fn conn_id_from_value(v: &Value) -> Option<u32> {
|
|
149
|
+
match v {
|
|
150
|
+
Value::Object(o) => {
|
|
151
|
+
let b = o.borrow();
|
|
152
|
+
// Direct conn: { _id, send, ... }
|
|
153
|
+
if let Some(idv) = b.get(&Arc::from("_id")) {
|
|
154
|
+
if let Value::Number(n) = idv {
|
|
155
|
+
if n.is_finite() && *n >= 0.0 {
|
|
156
|
+
return Some(*n as u32);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// Wrapper: { ws: conn, ... }
|
|
161
|
+
if let Some(ws) = b.get(&Arc::from("ws")) {
|
|
162
|
+
return conn_id_from_value(ws);
|
|
163
|
+
}
|
|
164
|
+
None
|
|
165
|
+
}
|
|
166
|
+
_ => None,
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/// Native broadcast: send data to all conns in array except `except`. Avoids Tish-side method calls.
|
|
171
|
+
pub fn ws_broadcast_native(args: &[Value]) -> Value {
|
|
172
|
+
let conns = match args.get(0) {
|
|
173
|
+
Some(Value::Array(a)) => a.borrow().clone(),
|
|
174
|
+
_ => return Value::Null,
|
|
175
|
+
};
|
|
176
|
+
let except = args.get(1).cloned().unwrap_or(Value::Null);
|
|
177
|
+
let data = args
|
|
178
|
+
.get(2)
|
|
179
|
+
.map(|v| v.to_display_string())
|
|
180
|
+
.unwrap_or_default();
|
|
181
|
+
let mut n = 0u32;
|
|
182
|
+
for c in conns {
|
|
183
|
+
if c.strict_eq(&except) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if let Some(id) = conn_id_from_value(&c) {
|
|
187
|
+
if conn_send(id, data.clone()) {
|
|
188
|
+
n += 1;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
Value::Number(n as f64)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/// Build connection object: { _id, send, close, readyState, receive }. JS-like.
|
|
196
|
+
fn conn_object(id: u32) -> Value {
|
|
197
|
+
let mut obj: HashMap<Arc<str>, Value> = HashMap::new();
|
|
198
|
+
obj.insert(Arc::from("_id"), Value::Number(id as f64));
|
|
199
|
+
obj.insert(Arc::from("readyState"), Value::Number(1.0)); // OPEN
|
|
200
|
+
obj.insert(
|
|
201
|
+
Arc::from("send"),
|
|
202
|
+
Value::Function(Rc::new(move |args: &[Value]| {
|
|
203
|
+
let data = args.first().map(|v| v.to_display_string()).unwrap_or_default();
|
|
204
|
+
Value::Bool(conn_send(id, data))
|
|
205
|
+
})),
|
|
206
|
+
);
|
|
207
|
+
obj.insert(
|
|
208
|
+
Arc::from("close"),
|
|
209
|
+
Value::Function(Rc::new(move |_args: &[Value]| {
|
|
210
|
+
unregister(id);
|
|
211
|
+
Value::Null
|
|
212
|
+
})),
|
|
213
|
+
);
|
|
214
|
+
obj.insert(
|
|
215
|
+
Arc::from("receive"),
|
|
216
|
+
Value::Function(Rc::new(move |_args: &[Value]| {
|
|
217
|
+
match conn_receive(id) {
|
|
218
|
+
Some(s) => {
|
|
219
|
+
let mut ev: HashMap<Arc<str>, Value> = HashMap::new();
|
|
220
|
+
ev.insert(Arc::from("data"), Value::String(s.into()));
|
|
221
|
+
Value::Object(Rc::new(RefCell::new(ev)))
|
|
222
|
+
}
|
|
223
|
+
None => Value::Null,
|
|
224
|
+
}
|
|
225
|
+
})),
|
|
226
|
+
);
|
|
227
|
+
let id_timeout = id;
|
|
228
|
+
obj.insert(
|
|
229
|
+
Arc::from("receiveTimeout"),
|
|
230
|
+
Value::Function(Rc::new(move |args: &[Value]| {
|
|
231
|
+
let timeout_ms = args
|
|
232
|
+
.first()
|
|
233
|
+
.and_then(|v| match v {
|
|
234
|
+
Value::Number(n) if n.is_finite() && *n >= 0.0 => Some((*n as u64).min(3600_000)),
|
|
235
|
+
_ => None,
|
|
236
|
+
})
|
|
237
|
+
.unwrap_or(1000);
|
|
238
|
+
match conn_receive_timeout(id_timeout, timeout_ms) {
|
|
239
|
+
Some(s) => {
|
|
240
|
+
let mut ev: HashMap<Arc<str>, Value> = HashMap::new();
|
|
241
|
+
ev.insert(Arc::from("data"), Value::String(s.into()));
|
|
242
|
+
Value::Object(Rc::new(RefCell::new(ev)))
|
|
243
|
+
}
|
|
244
|
+
None => Value::Null,
|
|
245
|
+
}
|
|
246
|
+
})),
|
|
247
|
+
);
|
|
248
|
+
Value::Object(Rc::new(RefCell::new(obj)))
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
fn parse_port(args: &[Value]) -> Option<u16> {
|
|
252
|
+
args.first().and_then(|v| match v {
|
|
253
|
+
Value::Object(o) => o.borrow().get(&Arc::from("port")).and_then(|v| match v {
|
|
254
|
+
Value::Number(n) if n.is_finite() && *n >= 0.0 => Some(*n as u16),
|
|
255
|
+
_ => None,
|
|
256
|
+
}),
|
|
257
|
+
_ => None,
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/// WebSocket(url) — JS-like client. Returns object with send, close, readyState, receive.
|
|
262
|
+
pub fn web_socket_client(args: &[Value]) -> Value {
|
|
263
|
+
let mut url = match args.first().map(|v| v.to_display_string()) {
|
|
264
|
+
Some(u) if !u.is_empty() => u,
|
|
265
|
+
_ => return Value::Null,
|
|
266
|
+
};
|
|
267
|
+
// Ensure URL has a path so the client sends "GET / ..." (avoids server responding with 200 instead of 101)
|
|
268
|
+
let after_scheme = url.find("://").map(|i| i + 3).unwrap_or(0);
|
|
269
|
+
if !url[after_scheme..].contains('/') {
|
|
270
|
+
url.push('/');
|
|
271
|
+
}
|
|
272
|
+
let (send_tx, mut send_rx) = tokio_mpsc::unbounded_channel::<String>();
|
|
273
|
+
let (recv_tx, recv_rx) = mpsc::sync_channel::<String>(64);
|
|
274
|
+
let recv_tx = Arc::new(recv_tx);
|
|
275
|
+
|
|
276
|
+
let id = with_ws_client_rt(|rt| {
|
|
277
|
+
rt.block_on(async move {
|
|
278
|
+
let (ws_stream, _) = match tokio_tungstenite::connect_async(&url).await {
|
|
279
|
+
Ok(x) => {
|
|
280
|
+
eprintln!("[tish ws] client connected (handshake OK): {}", url);
|
|
281
|
+
x
|
|
282
|
+
}
|
|
283
|
+
Err(e) => {
|
|
284
|
+
let hint = if e.to_string().contains("200 OK") {
|
|
285
|
+
" Another process may be using the port (not the WebSocket gateway). With gateway running, run: lsof -i :<port>"
|
|
286
|
+
} else {
|
|
287
|
+
""
|
|
288
|
+
};
|
|
289
|
+
eprintln!("[tish ws] connect_async failed: {} (url: {}){}", e, url, hint);
|
|
290
|
+
return None;
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
let id = register(send_tx, recv_rx);
|
|
294
|
+
let (mut write, mut read) = ws_stream.split();
|
|
295
|
+
let recv_tx = Arc::clone(&recv_tx);
|
|
296
|
+
let url_closed = url.clone();
|
|
297
|
+
tokio::spawn(async move {
|
|
298
|
+
while let Some(Ok(msg)) = read.next().await {
|
|
299
|
+
if let tokio_tungstenite::tungstenite::Message::Text(t) = msg {
|
|
300
|
+
let _ = recv_tx.send(t.to_string());
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
eprintln!("[tish ws] client connection closed (stream ended): {}", url_closed);
|
|
304
|
+
unregister(id);
|
|
305
|
+
});
|
|
306
|
+
tokio::spawn(async move {
|
|
307
|
+
while let Some(text) = send_rx.recv().await {
|
|
308
|
+
let _ = write
|
|
309
|
+
.send(tokio_tungstenite::tungstenite::Message::Text(text.into()))
|
|
310
|
+
.await;
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
Some(id)
|
|
314
|
+
})
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
let Some(id) = id else {
|
|
318
|
+
return Value::Null;
|
|
319
|
+
};
|
|
320
|
+
conn_object(id)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/// Start listening; returns `Value::Number(handle)` or `Value::Null` on bind failure.
|
|
324
|
+
/// A background thread accepts connections and pushes connection ids on a channel.
|
|
325
|
+
pub fn web_socket_server_listen(args: &[Value]) -> Value {
|
|
326
|
+
let port = match parse_port(args) {
|
|
327
|
+
Some(p) => p,
|
|
328
|
+
_ => return Value::Null,
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
let (bind_tx, bind_rx) = mpsc::sync_channel::<bool>(1);
|
|
332
|
+
let (conn_tx, conn_rx) = mpsc::channel::<u32>();
|
|
333
|
+
let handle = next_server_handle();
|
|
334
|
+
|
|
335
|
+
{
|
|
336
|
+
let mut map = SERVER_RECV.lock().unwrap();
|
|
337
|
+
map.insert(handle, conn_rx);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
std::thread::spawn(move || {
|
|
341
|
+
let rt = match tokio::runtime::Builder::new_current_thread()
|
|
342
|
+
.enable_all()
|
|
343
|
+
.build()
|
|
344
|
+
{
|
|
345
|
+
Ok(r) => r,
|
|
346
|
+
Err(_) => {
|
|
347
|
+
let _ = bind_tx.send(false);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
rt.block_on(async {
|
|
352
|
+
let listener = match tokio::net::TcpListener::bind(format!("0.0.0.0:{}", port)).await {
|
|
353
|
+
Ok(l) => l,
|
|
354
|
+
Err(_) => {
|
|
355
|
+
let _ = bind_tx.send(false);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
let _ = bind_tx.send(true);
|
|
360
|
+
println!("WebSocket server listening on ws://0.0.0.0:{}", port);
|
|
361
|
+
|
|
362
|
+
loop {
|
|
363
|
+
let (stream, _) = match listener.accept().await {
|
|
364
|
+
Ok(s) => s,
|
|
365
|
+
Err(_) => break,
|
|
366
|
+
};
|
|
367
|
+
let ws_stream = match tokio_tungstenite::accept_async(stream).await {
|
|
368
|
+
Ok(ws) => {
|
|
369
|
+
eprintln!("[tish ws] server accepted connection (handshake OK): port {}", port);
|
|
370
|
+
ws
|
|
371
|
+
}
|
|
372
|
+
Err(e) => {
|
|
373
|
+
eprintln!("[tish ws] server accept_async failed: {} (port {})", e, port);
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
let (send_tx, mut send_rx) = tokio_mpsc::unbounded_channel::<String>();
|
|
378
|
+
let (recv_tx, recv_rx) = mpsc::sync_channel::<String>(64);
|
|
379
|
+
let id = register(send_tx, recv_rx);
|
|
380
|
+
let recv_tx = Arc::new(recv_tx);
|
|
381
|
+
let recv_tx_task = Arc::clone(&recv_tx);
|
|
382
|
+
let (mut write, mut read) = ws_stream.split();
|
|
383
|
+
tokio::spawn(async move {
|
|
384
|
+
while let Some(Ok(msg)) = read.next().await {
|
|
385
|
+
if let tokio_tungstenite::tungstenite::Message::Text(t) = msg {
|
|
386
|
+
let _ = recv_tx_task.send(t.to_string());
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
unregister(id);
|
|
390
|
+
});
|
|
391
|
+
tokio::spawn(async move {
|
|
392
|
+
while let Some(text) = send_rx.recv().await {
|
|
393
|
+
let _ = write
|
|
394
|
+
.send(tokio_tungstenite::tungstenite::Message::Text(text.into()))
|
|
395
|
+
.await;
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
if conn_tx.send(id).is_err() {
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
match bind_rx.recv() {
|
|
406
|
+
Ok(true) => Value::Number(handle as f64),
|
|
407
|
+
_ => {
|
|
408
|
+
SERVER_RECV.lock().unwrap().remove(&handle);
|
|
409
|
+
Value::Null
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/// Block until the next connection for this server handle; returns connection object or `Null`.
|
|
415
|
+
pub fn web_socket_server_accept(args: &[Value]) -> Value {
|
|
416
|
+
let handle = match args.first() {
|
|
417
|
+
Some(Value::Number(n)) if n.is_finite() && *n >= 0.0 => *n as u32,
|
|
418
|
+
_ => return Value::Null,
|
|
419
|
+
};
|
|
420
|
+
let mut map = match SERVER_RECV.lock() {
|
|
421
|
+
Ok(g) => g,
|
|
422
|
+
Err(_) => return Value::Null,
|
|
423
|
+
};
|
|
424
|
+
let rx = match map.get_mut(&handle) {
|
|
425
|
+
Some(r) => r,
|
|
426
|
+
None => return Value::Null,
|
|
427
|
+
};
|
|
428
|
+
match rx.recv() {
|
|
429
|
+
Ok(id) => conn_object(id),
|
|
430
|
+
Err(_) => Value::Null,
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/// Like accept but with timeout (ms). Returns connection object or `Null` if no connection in time.
|
|
435
|
+
pub fn web_socket_server_accept_timeout(args: &[Value]) -> Value {
|
|
436
|
+
let handle = match args.first() {
|
|
437
|
+
Some(Value::Number(n)) if n.is_finite() && *n >= 0.0 => *n as u32,
|
|
438
|
+
_ => return Value::Null,
|
|
439
|
+
};
|
|
440
|
+
let timeout_ms = match args.get(1) {
|
|
441
|
+
Some(Value::Number(n)) if n.is_finite() && *n >= 0.0 => (*n as u64).min(3600_000),
|
|
442
|
+
_ => 100,
|
|
443
|
+
};
|
|
444
|
+
let mut map = match SERVER_RECV.lock() {
|
|
445
|
+
Ok(g) => g,
|
|
446
|
+
Err(_) => return Value::Null,
|
|
447
|
+
};
|
|
448
|
+
let rx = match map.get_mut(&handle) {
|
|
449
|
+
Some(r) => r,
|
|
450
|
+
None => return Value::Null,
|
|
451
|
+
};
|
|
452
|
+
match rx.recv_timeout(std::time::Duration::from_millis(timeout_ms)) {
|
|
453
|
+
Ok(id) => conn_object(id),
|
|
454
|
+
Err(_) => Value::Null,
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/// `Server(options)` — object with `_handle`, `_onConnection`, `on`, `listen`, `clients` (Node.js-compatible).
|
|
459
|
+
pub fn web_socket_server_construct(args: &[Value]) -> Value {
|
|
460
|
+
let handle_val = web_socket_server_listen(args);
|
|
461
|
+
if matches!(handle_val, Value::Null) {
|
|
462
|
+
return Value::Null;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Node.js-compatible: server.clients is array of connected WebSocket instances
|
|
466
|
+
let clients: Rc<RefCell<Vec<Value>>> = Rc::new(RefCell::new(Vec::new()));
|
|
467
|
+
|
|
468
|
+
let on_fn = Rc::new(|args: &[Value]| {
|
|
469
|
+
let Some(Value::Object(so)) = args.first() else {
|
|
470
|
+
return Value::Null;
|
|
471
|
+
};
|
|
472
|
+
let event = args
|
|
473
|
+
.get(1)
|
|
474
|
+
.map(|v| v.to_display_string())
|
|
475
|
+
.unwrap_or_default();
|
|
476
|
+
let cb = args.get(2).cloned().unwrap_or(Value::Null);
|
|
477
|
+
if event == "connection" {
|
|
478
|
+
so.borrow_mut()
|
|
479
|
+
.insert(Arc::from("_onConnection"), cb);
|
|
480
|
+
}
|
|
481
|
+
Value::Null
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
let clients_listen = Rc::clone(&clients);
|
|
485
|
+
let listen_fn = Rc::new(move |args: &[Value]| {
|
|
486
|
+
let Some(Value::Object(so)) = args.first() else {
|
|
487
|
+
return Value::Null;
|
|
488
|
+
};
|
|
489
|
+
loop {
|
|
490
|
+
let handle_n = {
|
|
491
|
+
let b = so.borrow();
|
|
492
|
+
match b.get(&Arc::from("_handle")).cloned().unwrap_or(Value::Null) {
|
|
493
|
+
Value::Number(n) if n.is_finite() && n >= 0.0 => n,
|
|
494
|
+
_ => break,
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
let cb = so
|
|
498
|
+
.borrow()
|
|
499
|
+
.get(&Arc::from("_onConnection"))
|
|
500
|
+
.cloned()
|
|
501
|
+
.unwrap_or(Value::Null);
|
|
502
|
+
let ws = web_socket_server_accept(&[Value::Number(handle_n)]);
|
|
503
|
+
if matches!(ws, Value::Null) {
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
clients_listen.borrow_mut().push(ws.clone());
|
|
507
|
+
if let Value::Function(f) = cb {
|
|
508
|
+
let _ = f(&[ws]);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
Value::Null
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
let clients_accept = Rc::clone(&clients);
|
|
515
|
+
let accept_timeout_fn = Rc::new(move |args: &[Value]| {
|
|
516
|
+
let Some(Value::Object(so)) = args.first() else {
|
|
517
|
+
return Value::Null;
|
|
518
|
+
};
|
|
519
|
+
let handle_n = so
|
|
520
|
+
.borrow()
|
|
521
|
+
.get(&Arc::from("_handle"))
|
|
522
|
+
.cloned()
|
|
523
|
+
.unwrap_or(Value::Null);
|
|
524
|
+
let timeout_ms = args.get(1).cloned().unwrap_or(Value::Number(100.0));
|
|
525
|
+
let ws = web_socket_server_accept_timeout(&[handle_n, timeout_ms]);
|
|
526
|
+
if !matches!(ws, Value::Null) {
|
|
527
|
+
clients_accept.borrow_mut().push(ws.clone());
|
|
528
|
+
}
|
|
529
|
+
ws
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
let mut m: HashMap<Arc<str>, Value> = HashMap::new();
|
|
533
|
+
m.insert(Arc::from("_handle"), handle_val);
|
|
534
|
+
m.insert(Arc::from("_onConnection"), Value::Null);
|
|
535
|
+
m.insert(Arc::from("clients"), Value::Array(clients));
|
|
536
|
+
m.insert(Arc::from("on"), Value::Function(on_fn));
|
|
537
|
+
m.insert(Arc::from("listen"), Value::Function(listen_fn));
|
|
538
|
+
m.insert(Arc::from("acceptTimeout"), Value::Function(accept_timeout_fn));
|
|
539
|
+
Value::Object(Rc::new(RefCell::new(m)))
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
#[cfg(test)]
|
|
543
|
+
mod tests {
|
|
544
|
+
use super::*;
|
|
545
|
+
use std::thread;
|
|
546
|
+
use std::time::Duration;
|
|
547
|
+
|
|
548
|
+
#[test]
|
|
549
|
+
fn ws_echo_roundtrip() {
|
|
550
|
+
let port: u16 = 18_742;
|
|
551
|
+
let opts = {
|
|
552
|
+
let mut m: HashMap<Arc<str>, Value> = HashMap::new();
|
|
553
|
+
m.insert(Arc::from("port"), Value::Number(port as f64));
|
|
554
|
+
Value::Object(Rc::new(RefCell::new(m)))
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
let handle = match web_socket_server_listen(std::slice::from_ref(&opts)) {
|
|
558
|
+
Value::Number(h) => h as u32,
|
|
559
|
+
_ => panic!("listen failed"),
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
let server = thread::spawn(move || {
|
|
563
|
+
let ws = web_socket_server_accept(&[Value::Number(handle as f64)]);
|
|
564
|
+
let Value::Object(wso) = ws else {
|
|
565
|
+
panic!("accept failed");
|
|
566
|
+
};
|
|
567
|
+
// Echo one message
|
|
568
|
+
for _ in 0..50 {
|
|
569
|
+
let recv_fn = wso.borrow().get(&Arc::from("receive")).cloned();
|
|
570
|
+
if let Some(Value::Function(rf)) = recv_fn {
|
|
571
|
+
let msg = rf(&[]);
|
|
572
|
+
if !matches!(msg, Value::Null) {
|
|
573
|
+
let data = match msg {
|
|
574
|
+
Value::Object(ev) => ev
|
|
575
|
+
.borrow()
|
|
576
|
+
.get(&Arc::from("data"))
|
|
577
|
+
.map(|v| v.to_display_string())
|
|
578
|
+
.unwrap_or_default(),
|
|
579
|
+
_ => String::new(),
|
|
580
|
+
};
|
|
581
|
+
if let Some(Value::Function(sf)) = wso.borrow().get(&Arc::from("send")).cloned() {
|
|
582
|
+
let _ = sf(&[Value::String(data.into())]);
|
|
583
|
+
}
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
thread::sleep(Duration::from_millis(10));
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
thread::sleep(Duration::from_millis(100));
|
|
592
|
+
let url = format!("ws://127.0.0.1:{}", port);
|
|
593
|
+
let client = web_socket_client(&[Value::String(url.into())]);
|
|
594
|
+
assert!(!matches!(client, Value::Null), "client connect failed");
|
|
595
|
+
|
|
596
|
+
let Value::Object(co) = client else {
|
|
597
|
+
panic!("client not object");
|
|
598
|
+
};
|
|
599
|
+
let send = co.borrow().get(&Arc::from("send")).cloned().unwrap();
|
|
600
|
+
let Value::Function(send_f) = send else {
|
|
601
|
+
panic!("no send");
|
|
602
|
+
};
|
|
603
|
+
let _ = send_f(&[Value::String("hello".into())]);
|
|
604
|
+
|
|
605
|
+
let recv = co.borrow().get(&Arc::from("receive")).cloned().unwrap();
|
|
606
|
+
let Value::Function(recv_f) = recv else {
|
|
607
|
+
panic!("no receive");
|
|
608
|
+
};
|
|
609
|
+
let mut got = Value::Null;
|
|
610
|
+
for _ in 0..100 {
|
|
611
|
+
got = recv_f(&[]);
|
|
612
|
+
if !matches!(got, Value::Null) {
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
thread::sleep(Duration::from_millis(10));
|
|
616
|
+
}
|
|
617
|
+
let Value::Object(ev) = got else {
|
|
618
|
+
panic!("expected message object");
|
|
619
|
+
};
|
|
620
|
+
let data = ev
|
|
621
|
+
.borrow()
|
|
622
|
+
.get(&Arc::from("data"))
|
|
623
|
+
.map(|v| v.to_display_string())
|
|
624
|
+
.unwrap_or_default();
|
|
625
|
+
assert_eq!(data, "hello");
|
|
626
|
+
|
|
627
|
+
let _ = server.join();
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/// Gateway→agent flow: server receives "join", sends "joined" + "presence"; client must receive both via receiveTimeout.
|
|
631
|
+
#[test]
|
|
632
|
+
fn ws_gateway_agent_flow() {
|
|
633
|
+
let port: u16 = 18_743;
|
|
634
|
+
let opts = {
|
|
635
|
+
let mut m: HashMap<Arc<str>, Value> = HashMap::new();
|
|
636
|
+
m.insert(Arc::from("port"), Value::Number(port as f64));
|
|
637
|
+
Value::Object(Rc::new(RefCell::new(m)))
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
let handle = match web_socket_server_listen(std::slice::from_ref(&opts)) {
|
|
641
|
+
Value::Number(h) => h as u32,
|
|
642
|
+
_ => panic!("listen failed"),
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
let server = thread::spawn(move || {
|
|
646
|
+
let ws = web_socket_server_accept(&[Value::Number(handle as f64)]);
|
|
647
|
+
let Value::Object(wso) = ws else {
|
|
648
|
+
panic!("accept failed");
|
|
649
|
+
};
|
|
650
|
+
let recv_fn = wso.borrow().get(&Arc::from("receive")).cloned();
|
|
651
|
+
let Value::Function(rf) = recv_fn.unwrap() else {
|
|
652
|
+
panic!("no receive");
|
|
653
|
+
};
|
|
654
|
+
// Poll until we get join
|
|
655
|
+
for _ in 0..200 {
|
|
656
|
+
let msg = rf(&[]);
|
|
657
|
+
if !matches!(msg, Value::Null) {
|
|
658
|
+
let data = match &msg {
|
|
659
|
+
Value::Object(ev) => ev
|
|
660
|
+
.borrow()
|
|
661
|
+
.get(&Arc::from("data"))
|
|
662
|
+
.map(|v| v.to_display_string())
|
|
663
|
+
.unwrap_or_default(),
|
|
664
|
+
_ => String::new(),
|
|
665
|
+
};
|
|
666
|
+
if data.contains("\"type\":\"join\"") || data.contains("\"type\": \"join\"") {
|
|
667
|
+
let joined = r#"{"type":"joined","sessionId":"default"}"#;
|
|
668
|
+
let presence = r#"{"type":"presence","agentLanes":["ai-a"]}"#;
|
|
669
|
+
ws_send_native(&Value::Object(Rc::clone(&wso)), joined);
|
|
670
|
+
ws_send_native(&Value::Object(Rc::clone(&wso)), presence);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
thread::sleep(Duration::from_millis(10));
|
|
675
|
+
}
|
|
676
|
+
panic!("server never got join");
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
thread::sleep(Duration::from_millis(100));
|
|
680
|
+
let url = format!("ws://127.0.0.1:{}/", port);
|
|
681
|
+
let client = web_socket_client(&[Value::String(url.into())]);
|
|
682
|
+
assert!(!matches!(client, Value::Null), "client connect failed");
|
|
683
|
+
|
|
684
|
+
let Value::Object(co) = client else {
|
|
685
|
+
panic!("client not object");
|
|
686
|
+
};
|
|
687
|
+
let send = co.borrow().get(&Arc::from("send")).cloned().unwrap();
|
|
688
|
+
let Value::Function(send_f) = send else {
|
|
689
|
+
panic!("no send");
|
|
690
|
+
};
|
|
691
|
+
let join_msg = r#"{"type":"join","sessionId":"default","role":"agent","laneId":"ai-a"}"#;
|
|
692
|
+
let _ = send_f(&[Value::String(join_msg.into())]);
|
|
693
|
+
|
|
694
|
+
// Client uses receiveTimeout like the agent
|
|
695
|
+
let recv_timeout = co.borrow().get(&Arc::from("receiveTimeout")).cloned().unwrap();
|
|
696
|
+
let Value::Function(recv_timeout_f) = recv_timeout else {
|
|
697
|
+
panic!("no receiveTimeout");
|
|
698
|
+
};
|
|
699
|
+
let timeout_arg = Value::Number(2000.0);
|
|
700
|
+
|
|
701
|
+
let got1 = recv_timeout_f(&[timeout_arg.clone()]);
|
|
702
|
+
let Value::Object(ev1) = got1 else {
|
|
703
|
+
panic!("first recv: expected object, got {:?}", got1);
|
|
704
|
+
};
|
|
705
|
+
let data1 = ev1
|
|
706
|
+
.borrow()
|
|
707
|
+
.get(&Arc::from("data"))
|
|
708
|
+
.map(|v| v.to_display_string())
|
|
709
|
+
.unwrap_or_default();
|
|
710
|
+
assert!(data1.contains("\"type\":\"joined\""), "expected joined, got {}", data1);
|
|
711
|
+
|
|
712
|
+
let got2 = recv_timeout_f(&[timeout_arg]);
|
|
713
|
+
let Value::Object(ev2) = got2 else {
|
|
714
|
+
panic!("second recv: expected object, got {:?}", got2);
|
|
715
|
+
};
|
|
716
|
+
let data2 = ev2
|
|
717
|
+
.borrow()
|
|
718
|
+
.get(&Arc::from("data"))
|
|
719
|
+
.map(|v| v.to_display_string())
|
|
720
|
+
.unwrap_or_default();
|
|
721
|
+
assert!(data2.contains("\"type\":\"presence\""), "expected presence, got {}", data2);
|
|
722
|
+
|
|
723
|
+
let _ = server.join();
|
|
724
|
+
}
|
|
725
|
+
}
|