@tishlang/tish-format 3.10.4 → 3.10.11
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/bin/tish-format +0 -0
- package/crates/tish/src/cargo_native_registry.rs +4 -0
- package/crates/tish/src/main.rs +65 -25
- package/crates/tish/tests/integration_test.rs +3 -0
- package/crates/tish_builtins/src/globals.rs +3 -3
- package/crates/tish_builtins/src/symbol.rs +86 -4
- package/crates/tish_bytecode/src/compiler.rs +65 -25
- package/crates/tish_bytecode/tests/captured_param_slot_binding.rs +131 -0
- package/crates/tish_bytecode/tests/fundecl_slot_binding.rs +115 -0
- package/crates/tish_bytecode/tests/param_defaults_slot_binding.rs +86 -0
- package/crates/tish_core/src/shape.rs +55 -2
- package/crates/tish_core/src/value.rs +229 -13
- package/crates/tish_eval/src/timers.rs +66 -2
- package/crates/tish_ffi/src/lib.rs +74 -12
- package/crates/tish_ffi/tests/loader.rs +35 -0
- package/crates/tish_fmt/Cargo.toml +1 -1
- package/crates/tish_lsp/src/main.rs +91 -2
- package/crates/tish_pg/src/lib.rs +59 -8
- package/crates/tish_pg/src/statement_registry.rs +250 -0
- package/crates/tish_runtime/src/http.rs +19 -0
- package/crates/tish_runtime/src/http_fetch.rs +159 -65
- package/crates/tish_runtime/src/http_hyper.rs +18 -2
- package/crates/tish_runtime/src/lib.rs +4 -0
- package/crates/tish_runtime/src/net.rs +125 -97
- package/crates/tish_runtime/src/process_spawn.rs +326 -113
- package/crates/tish_runtime/src/promise.rs +521 -90
- package/crates/tish_runtime/src/promise_io.rs +29 -6
- package/crates/tish_runtime/src/pty.rs +161 -107
- package/crates/tish_runtime/src/stream_buf.rs +419 -0
- package/crates/tish_runtime/src/ws.rs +431 -52
- package/crates/tish_vm/src/jit.rs +473 -67
- package/crates/tish_vm/src/lib.rs +6 -0
- package/crates/tish_vm/src/vm.rs +8 -5
- package/crates/tish_vm/tests/captured_param_slot_mode.rs +210 -0
- package/crates/tish_vm/tests/captured_param_vm_slots_off.rs +64 -0
- package/crates/tish_vm/tests/fundecl_slot_no_frame_cycle.rs +152 -0
- package/crates/tish_vm/tests/param_defaults_slot_mode.rs +126 -0
- package/crates/tish_vm/tests/param_defaults_vm_slots_off.rs +44 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-fmt +0 -0
- package/platform/darwin-x64/tish-fmt +0 -0
- package/platform/linux-arm64/tish-fmt +0 -0
- package/platform/linux-x64/tish-fmt +0 -0
- package/platform/win32-x64/tish-fmt.exe +0 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
//! Prepared-statement bookkeeping for the sync `cargo:tish_pg` facade.
|
|
2
|
+
//!
|
|
3
|
+
//! This is the `STATEMENTS` slab from `tish_sync`, extracted as a pure data
|
|
4
|
+
//! structure so its lifecycle semantics are unit-testable without a live
|
|
5
|
+
//! Postgres (`tokio_postgres::Statement` cannot be constructed outside a real
|
|
6
|
+
//! connection, so the registry is generic over the statement type `S`).
|
|
7
|
+
//!
|
|
8
|
+
//! Lifecycle contract (tishlang/tish#712):
|
|
9
|
+
//!
|
|
10
|
+
//! * `get` validates the stored client id — a handle prepared on one client
|
|
11
|
+
//! never resolves against another. `close()` frees CLIENTS slab indices
|
|
12
|
+
//! for reuse, so without this check a stale handle would be replayed
|
|
13
|
+
//! against whichever new connection reused the index.
|
|
14
|
+
//! * `insert` interns on `(client_id, sql)` so repeated prepares of
|
|
15
|
+
//! identical SQL reuse one entry — bounding the table by distinct SQL
|
|
16
|
+
//! text (program text) instead of call count.
|
|
17
|
+
//! * `sweep_client` drops every entry belonging to a closed client,
|
|
18
|
+
//! including its intern-map slice, so a later client that reuses the slab
|
|
19
|
+
//! index starts clean.
|
|
20
|
+
//! * `remove` (the `unprepare` export) frees a single entry.
|
|
21
|
+
//!
|
|
22
|
+
//! Removal hands the statement back to the caller: dropping a
|
|
23
|
+
//! `tokio_postgres::Statement` emits the server-side `DEALLOCATE` (`Close`
|
|
24
|
+
//! message) while its connection is still alive, so "drop what sweep/remove
|
|
25
|
+
//! return" is the deallocation path.
|
|
26
|
+
|
|
27
|
+
use slab::Slab;
|
|
28
|
+
use std::collections::HashMap;
|
|
29
|
+
|
|
30
|
+
pub(crate) struct StatementRegistry<S> {
|
|
31
|
+
/// stmt_id -> (client_id, sql, statement). The SQL text rides along so
|
|
32
|
+
/// `remove`/`sweep_client` can purge the matching intern entry; `get`
|
|
33
|
+
/// clones only the statement, never the SQL, keeping the per-query hot
|
|
34
|
+
/// path allocation-free.
|
|
35
|
+
slab: Slab<(usize, String, S)>,
|
|
36
|
+
/// client_id -> sql -> stmt_id. Nested (rather than keyed on a
|
|
37
|
+
/// `(usize, String)` tuple) so `lookup` borrows the SQL without
|
|
38
|
+
/// allocating and `sweep_client` drops a client's slice in O(1).
|
|
39
|
+
intern: HashMap<usize, HashMap<String, usize>>,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
impl<S: Clone> StatementRegistry<S> {
|
|
43
|
+
pub(crate) fn new() -> Self {
|
|
44
|
+
Self {
|
|
45
|
+
slab: Slab::new(),
|
|
46
|
+
intern: HashMap::new(),
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Register a statement prepared on `client_id` for `sql`.
|
|
51
|
+
///
|
|
52
|
+
/// Idempotent per `(client_id, sql)`: if an entry is already interned
|
|
53
|
+
/// its existing handle is returned and `stmt` is discarded — for a
|
|
54
|
+
/// `tokio_postgres::Statement` the drop emits the server-side
|
|
55
|
+
/// `DEALLOCATE`, so losing a race to a concurrent identical `prepare`
|
|
56
|
+
/// leaves nothing pinned on either side.
|
|
57
|
+
pub(crate) fn insert(&mut self, client_id: usize, sql: &str, stmt: S) -> usize {
|
|
58
|
+
if let Some(id) = self.lookup(client_id, sql) {
|
|
59
|
+
return id;
|
|
60
|
+
}
|
|
61
|
+
let id = self.slab.insert((client_id, sql.to_string(), stmt));
|
|
62
|
+
self.intern
|
|
63
|
+
.entry(client_id)
|
|
64
|
+
.or_default()
|
|
65
|
+
.insert(sql.to_string(), id);
|
|
66
|
+
id
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// Resolve a statement handle on behalf of `client_id`.
|
|
70
|
+
///
|
|
71
|
+
/// A mismatch between the stored and presented client id is a MISS:
|
|
72
|
+
/// after `close()` recycles a CLIENTS slab index, a stale statement
|
|
73
|
+
/// handle must not replay against the new connection.
|
|
74
|
+
pub(crate) fn get(&self, stmt_id: usize, client_id: usize) -> Option<S> {
|
|
75
|
+
match self.slab.get(stmt_id) {
|
|
76
|
+
Some((cid, _sql, stmt)) if *cid == client_id => Some(stmt.clone()),
|
|
77
|
+
_ => None,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// Free one statement entry (the `unprepare` export), returning the
|
|
82
|
+
/// statement so the caller can drop it (which sends `DEALLOCATE`).
|
|
83
|
+
pub(crate) fn remove(&mut self, stmt_id: usize) -> Option<S> {
|
|
84
|
+
if !self.slab.contains(stmt_id) {
|
|
85
|
+
return None;
|
|
86
|
+
}
|
|
87
|
+
let (cid, sql, stmt) = self.slab.remove(stmt_id);
|
|
88
|
+
if let Some(per_client) = self.intern.get_mut(&cid) {
|
|
89
|
+
per_client.remove(&sql);
|
|
90
|
+
if per_client.is_empty() {
|
|
91
|
+
self.intern.remove(&cid);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
Some(stmt)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Drop every entry belonging to `client_id` (the `close()` sweep);
|
|
98
|
+
/// returns how many were removed. The dropped statements emit their
|
|
99
|
+
/// `DEALLOCATE`s if the connection still lives; purging the intern
|
|
100
|
+
/// slice keeps a later client that reuses this index from inheriting
|
|
101
|
+
/// stale handles.
|
|
102
|
+
pub(crate) fn sweep_client(&mut self, client_id: usize) -> usize {
|
|
103
|
+
let before = self.slab.len();
|
|
104
|
+
self.slab.retain(|_, (cid, _sql, _stmt)| *cid != client_id);
|
|
105
|
+
self.intern.remove(&client_id);
|
|
106
|
+
before - self.slab.len()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// Find the interned handle for `(client_id, sql)`, if one exists.
|
|
110
|
+
pub(crate) fn lookup(&self, client_id: usize, sql: &str) -> Option<usize> {
|
|
111
|
+
self.intern.get(&client_id)?.get(sql).copied()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#[cfg(test)]
|
|
115
|
+
pub(crate) fn len(&self) -> usize {
|
|
116
|
+
self.slab.len()
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#[cfg(test)]
|
|
121
|
+
mod tests {
|
|
122
|
+
use super::StatementRegistry;
|
|
123
|
+
|
|
124
|
+
fn s(x: &str) -> String {
|
|
125
|
+
x.to_string()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#[test]
|
|
129
|
+
fn get_with_matching_client_id_hits() {
|
|
130
|
+
let mut r = StatementRegistry::new();
|
|
131
|
+
let id = r.insert(3, "SELECT 1", s("stmt-a"));
|
|
132
|
+
assert_eq!(r.get(id, 3), Some(s("stmt-a")));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#[test]
|
|
136
|
+
fn get_with_mismatched_client_id_misses() {
|
|
137
|
+
// The stale-replay guard: a handle prepared on client 3 must never
|
|
138
|
+
// resolve for client 4. After close() frees a CLIENTS slab index for
|
|
139
|
+
// reuse, an old statement handle would otherwise be replayed against
|
|
140
|
+
// whichever new connection reused the index.
|
|
141
|
+
let mut r = StatementRegistry::new();
|
|
142
|
+
let id = r.insert(3, "SELECT 1", s("stmt-a"));
|
|
143
|
+
assert_eq!(
|
|
144
|
+
r.get(id, 4),
|
|
145
|
+
None,
|
|
146
|
+
"statement prepared on client 3 must MISS when resolved for client 4"
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#[test]
|
|
151
|
+
fn close_sweep_removes_only_that_clients_statements() {
|
|
152
|
+
let mut r = StatementRegistry::new();
|
|
153
|
+
let a = r.insert(1, "SELECT 1", s("c1-a"));
|
|
154
|
+
let b = r.insert(1, "SELECT 2", s("c1-b"));
|
|
155
|
+
let c = r.insert(2, "SELECT 1", s("c2-a"));
|
|
156
|
+
let swept = r.sweep_client(1);
|
|
157
|
+
assert_eq!(swept, 2, "both of client 1's statements must be swept");
|
|
158
|
+
assert_eq!(r.get(a, 1), None);
|
|
159
|
+
assert_eq!(r.get(b, 1), None);
|
|
160
|
+
assert_eq!(r.get(c, 2), Some(s("c2-a")), "client 2 must be untouched");
|
|
161
|
+
assert_eq!(r.len(), 1);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
#[test]
|
|
165
|
+
fn unprepare_removes_entry() {
|
|
166
|
+
let mut r = StatementRegistry::new();
|
|
167
|
+
let id = r.insert(1, "SELECT 1", s("stmt-a"));
|
|
168
|
+
assert_eq!(
|
|
169
|
+
r.remove(id),
|
|
170
|
+
Some(s("stmt-a")),
|
|
171
|
+
"remove must yield the statement so its Drop can DEALLOCATE"
|
|
172
|
+
);
|
|
173
|
+
assert_eq!(r.get(id, 1), None);
|
|
174
|
+
assert_eq!(r.remove(id), None, "double-unprepare is a miss");
|
|
175
|
+
assert_eq!(r.len(), 0);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#[test]
|
|
179
|
+
fn reprepare_same_sql_reuses_interned_entry() {
|
|
180
|
+
// prepare() must be idempotent per (client, sql): the misuse pattern
|
|
181
|
+
// of preparing inside a request handler must not grow the table.
|
|
182
|
+
let mut r = StatementRegistry::new();
|
|
183
|
+
let first = r.insert(1, "SELECT 1", s("stmt-a"));
|
|
184
|
+
let second = r.insert(1, "SELECT 1", s("stmt-b"));
|
|
185
|
+
assert_eq!(
|
|
186
|
+
second, first,
|
|
187
|
+
"identical (client, sql) must reuse the entry"
|
|
188
|
+
);
|
|
189
|
+
assert_eq!(r.len(), 1);
|
|
190
|
+
// The original statement wins; the racing duplicate is dropped by the
|
|
191
|
+
// caller (its Drop emits the server-side DEALLOCATE).
|
|
192
|
+
assert_eq!(r.get(first, 1), Some(s("stmt-a")));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
#[test]
|
|
196
|
+
fn distinct_sql_or_client_gets_distinct_entries() {
|
|
197
|
+
// The intern bound is (client, distinct SQL) — different SQL on the
|
|
198
|
+
// same client, and the same SQL on different clients, each get their
|
|
199
|
+
// own entry.
|
|
200
|
+
let mut r = StatementRegistry::new();
|
|
201
|
+
let a = r.insert(1, "SELECT 1", s("c1-sel1"));
|
|
202
|
+
let b = r.insert(1, "SELECT 2", s("c1-sel2"));
|
|
203
|
+
let c = r.insert(2, "SELECT 1", s("c2-sel1"));
|
|
204
|
+
assert_ne!(a, b);
|
|
205
|
+
assert_ne!(a, c);
|
|
206
|
+
assert_eq!(r.len(), 3);
|
|
207
|
+
assert_eq!(r.get(a, 1), Some(s("c1-sel1")));
|
|
208
|
+
assert_eq!(r.get(b, 1), Some(s("c1-sel2")));
|
|
209
|
+
assert_eq!(r.get(c, 2), Some(s("c2-sel1")));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
#[test]
|
|
213
|
+
fn lookup_finds_interned_entry() {
|
|
214
|
+
let mut r = StatementRegistry::new();
|
|
215
|
+
assert_eq!(r.lookup(1, "SELECT 1"), None);
|
|
216
|
+
let id = r.insert(1, "SELECT 1", s("stmt-a"));
|
|
217
|
+
assert_eq!(r.lookup(1, "SELECT 1"), Some(id));
|
|
218
|
+
assert_eq!(r.lookup(2, "SELECT 1"), None, "interning is per client");
|
|
219
|
+
assert_eq!(r.lookup(1, "SELECT 2"), None, "interning is per SQL text");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
#[test]
|
|
223
|
+
fn sweep_purges_intern_state_for_reused_client_index() {
|
|
224
|
+
// After close(client 0), a NEW client that reuses slab index 0 must
|
|
225
|
+
// not inherit the old client's statements or interned handles.
|
|
226
|
+
let mut r = StatementRegistry::new();
|
|
227
|
+
r.insert(0, "SELECT 1", s("old-stmt"));
|
|
228
|
+
r.sweep_client(0);
|
|
229
|
+
assert_eq!(
|
|
230
|
+
r.lookup(0, "SELECT 1"),
|
|
231
|
+
None,
|
|
232
|
+
"a reused client index must re-prepare, not inherit the old client's intern entry"
|
|
233
|
+
);
|
|
234
|
+
assert_eq!(r.len(), 0, "the closed client's statements must be gone");
|
|
235
|
+
let new_id = r.insert(0, "SELECT 1", s("new-stmt"));
|
|
236
|
+
assert_eq!(r.get(new_id, 0), Some(s("new-stmt")));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
#[test]
|
|
240
|
+
fn unprepare_then_reprepare_gets_fresh_entry() {
|
|
241
|
+
// remove() must purge the intern entry too, or a later prepare of
|
|
242
|
+
// the same SQL would return a dangling handle.
|
|
243
|
+
let mut r = StatementRegistry::new();
|
|
244
|
+
let id = r.insert(1, "SELECT 1", s("stmt-a"));
|
|
245
|
+
r.remove(id);
|
|
246
|
+
assert_eq!(r.lookup(1, "SELECT 1"), None);
|
|
247
|
+
let id2 = r.insert(1, "SELECT 1", s("stmt-b"));
|
|
248
|
+
assert_eq!(r.get(id2, 1), Some(s("stmt-b")));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
@@ -64,6 +64,25 @@ where
|
|
|
64
64
|
})
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/// Runtime hosting settlement-subscription waiters registered by `Promise.race` /
|
|
68
|
+
/// `Promise.any` / `Promise.allSettled` on oneshot-backed fetch/IO promises
|
|
69
|
+
/// (`TishPromise::subscribe`, issue #702). Process-global and immortal ON PURPOSE:
|
|
70
|
+
/// a subscription must be able to outlive the subscribing thread (the promise may
|
|
71
|
+
/// settle long after the racer moved on), so it cannot live on the thread-local
|
|
72
|
+
/// [`RUNTIME`]. One lazily-created worker thread total, shared by every waiter —
|
|
73
|
+
/// never one per pending input.
|
|
74
|
+
pub(crate) fn subscriber_runtime() -> &'static Runtime {
|
|
75
|
+
static RT: OnceLock<Runtime> = OnceLock::new();
|
|
76
|
+
RT.get_or_init(|| {
|
|
77
|
+
tokio::runtime::Builder::new_multi_thread()
|
|
78
|
+
.worker_threads(1)
|
|
79
|
+
.thread_name("tish-promise-subscriber")
|
|
80
|
+
.enable_all()
|
|
81
|
+
.build()
|
|
82
|
+
.expect("Failed to create promise subscriber runtime")
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
67
86
|
pub fn await_fetch(args: Vec<Value>) -> Value {
|
|
68
87
|
crate::promise::await_promise(crate::native_promise::fetch_promise(args))
|
|
69
88
|
}
|
|
@@ -15,24 +15,54 @@ use crate::http::{
|
|
|
15
15
|
MultipartPart,
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
// --- Promises (Send payloads only; Value built on awaiting thread) ---
|
|
18
|
+
// --- Promises (Send payloads only; Value built on awaiting/settling thread) ---
|
|
19
|
+
//
|
|
20
|
+
// These are one-shot by construction (the oneshot receiver is taken by the first
|
|
21
|
+
// await OR the first race subscription). `subscribe` awaits the oneshot as a task
|
|
22
|
+
// on the shared subscriber runtime instead of parking a blocking OS thread (plus
|
|
23
|
+
// a per-call tokio runtime) per raced input — issue #702.
|
|
24
|
+
|
|
25
|
+
fn consumed() -> Value {
|
|
26
|
+
Value::String("Promise already consumed".into())
|
|
27
|
+
}
|
|
19
28
|
|
|
20
29
|
struct FetchResponsePromise {
|
|
21
30
|
rx: Mutex<Option<tokio::sync::oneshot::Receiver<Result<reqwest::Response, String>>>>,
|
|
22
31
|
}
|
|
23
32
|
|
|
33
|
+
impl FetchResponsePromise {
|
|
34
|
+
fn convert(
|
|
35
|
+
r: Result<Result<reqwest::Response, String>, tokio::sync::oneshot::error::RecvError>,
|
|
36
|
+
) -> std::result::Result<Value, Value> {
|
|
37
|
+
match r {
|
|
38
|
+
Ok(Ok(resp)) => Ok(response_value_from_reqwest(resp)),
|
|
39
|
+
Ok(Err(e)) => Ok(build_error_response(&e)),
|
|
40
|
+
Err(_) => Err(Value::String("Promise dropped".into())),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
24
45
|
impl TishPromise for FetchResponsePromise {
|
|
25
46
|
fn block_until_settled(&self) -> std::result::Result<Value, Value> {
|
|
26
47
|
let rx = self.rx.lock().unwrap().take();
|
|
27
48
|
if let Some(rx) = rx {
|
|
28
|
-
|
|
29
|
-
match r {
|
|
30
|
-
Ok(Ok(resp)) => Ok(response_value_from_reqwest(resp)),
|
|
31
|
-
Ok(Err(e)) => Ok(build_error_response(&e)),
|
|
32
|
-
Err(_) => Err(Value::String("Promise dropped".into())),
|
|
33
|
-
}
|
|
49
|
+
Self::convert(crate::http::block_on_http(rx))
|
|
34
50
|
} else {
|
|
35
|
-
Err(
|
|
51
|
+
Err(consumed())
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fn subscribe(
|
|
56
|
+
self: Arc<Self>,
|
|
57
|
+
on_settled: Box<dyn FnOnce(std::result::Result<Value, Value>) + Send>,
|
|
58
|
+
) {
|
|
59
|
+
match self.rx.lock().unwrap().take() {
|
|
60
|
+
Some(rx) => {
|
|
61
|
+
crate::http::subscriber_runtime().spawn(async move {
|
|
62
|
+
on_settled(Self::convert(rx.await));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
None => on_settled(Err(consumed())),
|
|
36
66
|
}
|
|
37
67
|
}
|
|
38
68
|
}
|
|
@@ -46,27 +76,52 @@ struct FetchAllResponsesPromise {
|
|
|
46
76
|
>,
|
|
47
77
|
}
|
|
48
78
|
|
|
79
|
+
impl FetchAllResponsesPromise {
|
|
80
|
+
#[allow(clippy::type_complexity)]
|
|
81
|
+
fn convert(
|
|
82
|
+
r: Result<
|
|
83
|
+
Result<Vec<Result<reqwest::Response, String>>, String>,
|
|
84
|
+
tokio::sync::oneshot::error::RecvError,
|
|
85
|
+
>,
|
|
86
|
+
) -> std::result::Result<Value, Value> {
|
|
87
|
+
match r {
|
|
88
|
+
Ok(Ok(vec)) => {
|
|
89
|
+
let out: Vec<Value> = vec
|
|
90
|
+
.into_iter()
|
|
91
|
+
.map(|x| {
|
|
92
|
+
x.map(response_value_from_reqwest)
|
|
93
|
+
.unwrap_or_else(|e| build_error_response(&e))
|
|
94
|
+
})
|
|
95
|
+
.collect();
|
|
96
|
+
Ok(Value::Array(VmRef::new(out)))
|
|
97
|
+
}
|
|
98
|
+
Ok(Err(e)) => Ok(build_error_response(&e)),
|
|
99
|
+
Err(_) => Err(Value::String("Promise dropped".into())),
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
49
104
|
impl TishPromise for FetchAllResponsesPromise {
|
|
50
105
|
fn block_until_settled(&self) -> std::result::Result<Value, Value> {
|
|
51
106
|
let rx = self.rx.lock().unwrap().take();
|
|
52
107
|
if let Some(rx) = rx {
|
|
53
|
-
|
|
54
|
-
match r {
|
|
55
|
-
Ok(Ok(vec)) => {
|
|
56
|
-
let out: Vec<Value> = vec
|
|
57
|
-
.into_iter()
|
|
58
|
-
.map(|x| {
|
|
59
|
-
x.map(response_value_from_reqwest)
|
|
60
|
-
.unwrap_or_else(|e| build_error_response(&e))
|
|
61
|
-
})
|
|
62
|
-
.collect();
|
|
63
|
-
Ok(Value::Array(VmRef::new(out)))
|
|
64
|
-
}
|
|
65
|
-
Ok(Err(e)) => Ok(build_error_response(&e)),
|
|
66
|
-
Err(_) => Err(Value::String("Promise dropped".into())),
|
|
67
|
-
}
|
|
108
|
+
Self::convert(crate::http::block_on_http(rx))
|
|
68
109
|
} else {
|
|
69
|
-
Err(
|
|
110
|
+
Err(consumed())
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fn subscribe(
|
|
115
|
+
self: Arc<Self>,
|
|
116
|
+
on_settled: Box<dyn FnOnce(std::result::Result<Value, Value>) + Send>,
|
|
117
|
+
) {
|
|
118
|
+
match self.rx.lock().unwrap().take() {
|
|
119
|
+
Some(rx) => {
|
|
120
|
+
crate::http::subscriber_runtime().spawn(async move {
|
|
121
|
+
on_settled(Self::convert(rx.await));
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
None => on_settled(Err(consumed())),
|
|
70
125
|
}
|
|
71
126
|
}
|
|
72
127
|
}
|
|
@@ -80,34 +135,55 @@ struct ReadChunkPromise {
|
|
|
80
135
|
rx: Mutex<Option<tokio::sync::oneshot::Receiver<Result<ReadChunk, String>>>>,
|
|
81
136
|
}
|
|
82
137
|
|
|
138
|
+
impl ReadChunkPromise {
|
|
139
|
+
fn convert(
|
|
140
|
+
r: Result<Result<ReadChunk, String>, tokio::sync::oneshot::error::RecvError>,
|
|
141
|
+
) -> std::result::Result<Value, Value> {
|
|
142
|
+
match r {
|
|
143
|
+
Ok(Ok(ReadChunk::Done)) => {
|
|
144
|
+
let mut o = ObjectMap::default();
|
|
145
|
+
o.insert(Arc::from("done"), Value::Bool(true));
|
|
146
|
+
o.insert(Arc::from("value"), Value::Null);
|
|
147
|
+
Ok(Value::object(o))
|
|
148
|
+
}
|
|
149
|
+
Ok(Ok(ReadChunk::Bytes(b))) => {
|
|
150
|
+
let arr: Vec<Value> = b.iter().map(|u| Value::Number(*u as f64)).collect();
|
|
151
|
+
let mut o = ObjectMap::default();
|
|
152
|
+
o.insert(Arc::from("done"), Value::Bool(false));
|
|
153
|
+
o.insert(Arc::from("value"), Value::Array(VmRef::new(arr)));
|
|
154
|
+
Ok(Value::object(o))
|
|
155
|
+
}
|
|
156
|
+
Ok(Err(e)) => Err({
|
|
157
|
+
let mut obj = ObjectMap::default();
|
|
158
|
+
obj.insert(Arc::from("error"), Value::String(e.into()));
|
|
159
|
+
Value::object(obj)
|
|
160
|
+
}),
|
|
161
|
+
Err(_) => Err(Value::String("Promise dropped".into())),
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
83
166
|
impl TishPromise for ReadChunkPromise {
|
|
84
167
|
fn block_until_settled(&self) -> std::result::Result<Value, Value> {
|
|
85
168
|
let rx = self.rx.lock().unwrap().take();
|
|
86
169
|
if let Some(rx) = rx {
|
|
87
|
-
|
|
88
|
-
match r {
|
|
89
|
-
Ok(Ok(ReadChunk::Done)) => {
|
|
90
|
-
let mut o = ObjectMap::default();
|
|
91
|
-
o.insert(Arc::from("done"), Value::Bool(true));
|
|
92
|
-
o.insert(Arc::from("value"), Value::Null);
|
|
93
|
-
Ok(Value::object(o))
|
|
94
|
-
}
|
|
95
|
-
Ok(Ok(ReadChunk::Bytes(b))) => {
|
|
96
|
-
let arr: Vec<Value> = b.iter().map(|u| Value::Number(*u as f64)).collect();
|
|
97
|
-
let mut o = ObjectMap::default();
|
|
98
|
-
o.insert(Arc::from("done"), Value::Bool(false));
|
|
99
|
-
o.insert(Arc::from("value"), Value::Array(VmRef::new(arr)));
|
|
100
|
-
Ok(Value::object(o))
|
|
101
|
-
}
|
|
102
|
-
Ok(Err(e)) => Err({
|
|
103
|
-
let mut obj = ObjectMap::default();
|
|
104
|
-
obj.insert(Arc::from("error"), Value::String(e.into()));
|
|
105
|
-
Value::object(obj)
|
|
106
|
-
}),
|
|
107
|
-
Err(_) => Err(Value::String("Promise dropped".into())),
|
|
108
|
-
}
|
|
170
|
+
Self::convert(crate::http::block_on_http(rx))
|
|
109
171
|
} else {
|
|
110
|
-
Err(
|
|
172
|
+
Err(consumed())
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fn subscribe(
|
|
177
|
+
self: Arc<Self>,
|
|
178
|
+
on_settled: Box<dyn FnOnce(std::result::Result<Value, Value>) + Send>,
|
|
179
|
+
) {
|
|
180
|
+
match self.rx.lock().unwrap().take() {
|
|
181
|
+
Some(rx) => {
|
|
182
|
+
crate::http::subscriber_runtime().spawn(async move {
|
|
183
|
+
on_settled(Self::convert(rx.await));
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
None => on_settled(Err(consumed())),
|
|
111
187
|
}
|
|
112
188
|
}
|
|
113
189
|
}
|
|
@@ -116,29 +192,47 @@ struct JsonTextPromise {
|
|
|
116
192
|
rx: Mutex<Option<tokio::sync::oneshot::Receiver<Result<String, String>>>>,
|
|
117
193
|
}
|
|
118
194
|
|
|
195
|
+
impl JsonTextPromise {
|
|
196
|
+
fn convert(
|
|
197
|
+
r: Result<Result<String, String>, tokio::sync::oneshot::error::RecvError>,
|
|
198
|
+
) -> std::result::Result<Value, Value> {
|
|
199
|
+
fn error_obj(e: String) -> Value {
|
|
200
|
+
let mut obj = ObjectMap::default();
|
|
201
|
+
obj.insert(Arc::from("error"), Value::String(e.into()));
|
|
202
|
+
Value::object(obj)
|
|
203
|
+
}
|
|
204
|
+
match r {
|
|
205
|
+
Ok(Ok(s)) => match tishlang_core::json_parse(&s) {
|
|
206
|
+
Ok(v) => Ok(v),
|
|
207
|
+
Err(e) => Err(error_obj(e)),
|
|
208
|
+
},
|
|
209
|
+
Ok(Err(e)) => Err(error_obj(e)),
|
|
210
|
+
Err(_) => Err(Value::String("Promise dropped".into())),
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
119
215
|
impl TishPromise for JsonTextPromise {
|
|
120
216
|
fn block_until_settled(&self) -> std::result::Result<Value, Value> {
|
|
121
217
|
let rx = self.rx.lock().unwrap().take();
|
|
122
218
|
if let Some(rx) = rx {
|
|
123
|
-
|
|
124
|
-
match r {
|
|
125
|
-
Ok(Ok(s)) => match tishlang_core::json_parse(&s) {
|
|
126
|
-
Ok(v) => Ok(v),
|
|
127
|
-
Err(e) => Err({
|
|
128
|
-
let mut obj = ObjectMap::default();
|
|
129
|
-
obj.insert(Arc::from("error"), Value::String(e.into()));
|
|
130
|
-
Value::object(obj)
|
|
131
|
-
}),
|
|
132
|
-
},
|
|
133
|
-
Ok(Err(e)) => Err({
|
|
134
|
-
let mut obj = ObjectMap::default();
|
|
135
|
-
obj.insert(Arc::from("error"), Value::String(e.into()));
|
|
136
|
-
Value::object(obj)
|
|
137
|
-
}),
|
|
138
|
-
Err(_) => Err(Value::String("Promise dropped".into())),
|
|
139
|
-
}
|
|
219
|
+
Self::convert(crate::http::block_on_http(rx))
|
|
140
220
|
} else {
|
|
141
|
-
Err(
|
|
221
|
+
Err(consumed())
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
fn subscribe(
|
|
226
|
+
self: Arc<Self>,
|
|
227
|
+
on_settled: Box<dyn FnOnce(std::result::Result<Value, Value>) + Send>,
|
|
228
|
+
) {
|
|
229
|
+
match self.rx.lock().unwrap().take() {
|
|
230
|
+
Some(rx) => {
|
|
231
|
+
crate::http::subscriber_runtime().spawn(async move {
|
|
232
|
+
on_settled(Self::convert(rx.await));
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
None => on_settled(Err(consumed())),
|
|
142
236
|
}
|
|
143
237
|
}
|
|
144
238
|
}
|
|
@@ -491,6 +491,17 @@ fn handle_ws_upgrade(mut req: Request<hyper::body::Incoming>) -> Response<Full<B
|
|
|
491
491
|
use tokio_tungstenite::tungstenite::handshake::derive_accept_key;
|
|
492
492
|
use tokio_tungstenite::tungstenite::protocol::Role;
|
|
493
493
|
|
|
494
|
+
// Enforce the same connection cap as the tish:ws listener BEFORE the 101. This path runs
|
|
495
|
+
// pre-auth — the VM handler never sees the request — so without the check an unauthenticated
|
|
496
|
+
// client could register unbounded permanent connections (tasks + fd + buffers each) on any
|
|
497
|
+
// serve() app built with http-hyper + ws (#707).
|
|
498
|
+
if !crate::ws::has_ws_capacity() {
|
|
499
|
+
return simple_error_response(
|
|
500
|
+
StatusCode::SERVICE_UNAVAILABLE,
|
|
501
|
+
"websocket connection limit reached",
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
494
505
|
let accept = match req.headers().get(hyper::header::SEC_WEBSOCKET_KEY) {
|
|
495
506
|
Some(k) => derive_accept_key(k.as_bytes()),
|
|
496
507
|
None => return simple_error_response(StatusCode::BAD_REQUEST, "missing Sec-WebSocket-Key"),
|
|
@@ -501,8 +512,13 @@ fn handle_ws_upgrade(mut req: Request<hyper::body::Incoming>) -> Response<Full<B
|
|
|
501
512
|
match on_upgrade.await {
|
|
502
513
|
Ok(upgraded) => {
|
|
503
514
|
let io = TokioIo::new(upgraded);
|
|
504
|
-
|
|
505
|
-
|
|
515
|
+
// Shared config: 16 KiB buffers instead of tungstenite's eager 128 KiB default.
|
|
516
|
+
let ws = tokio_tungstenite::WebSocketStream::from_raw_socket(
|
|
517
|
+
io,
|
|
518
|
+
Role::Server,
|
|
519
|
+
Some(crate::ws::ws_config()),
|
|
520
|
+
)
|
|
521
|
+
.await;
|
|
506
522
|
let id = crate::ws::register_ws_stream(ws);
|
|
507
523
|
crate::ws::enqueue_upgraded_conn(id);
|
|
508
524
|
}
|
|
@@ -1773,6 +1773,10 @@ pub use tty::{
|
|
|
1773
1773
|
tty_enter_alt_screen, tty_is_tty, tty_leave_alt_screen, tty_read, tty_read_line, tty_set_raw_mode, tty_size,
|
|
1774
1774
|
};
|
|
1775
1775
|
|
|
1776
|
+
/// Shared reader-thread buffer + UTF-8 drain for the byte-stream modules (pty / process / net).
|
|
1777
|
+
#[cfg(any(feature = "pty", feature = "process", feature = "net"))]
|
|
1778
|
+
mod stream_buf;
|
|
1779
|
+
|
|
1776
1780
|
#[cfg(feature = "pty")]
|
|
1777
1781
|
pub mod pty;
|
|
1778
1782
|
#[cfg(feature = "pty")]
|