@tishlang/tish-format 1.0.12 → 1.0.13

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.
Files changed (164) hide show
  1. package/Cargo.toml +49 -0
  2. package/LICENSE +13 -0
  3. package/README.md +138 -0
  4. package/bin/tish-format +0 -0
  5. package/crates/js_to_tish/Cargo.toml +11 -0
  6. package/crates/js_to_tish/README.md +18 -0
  7. package/crates/js_to_tish/src/error.rs +55 -0
  8. package/crates/js_to_tish/src/lib.rs +11 -0
  9. package/crates/js_to_tish/src/span_util.rs +35 -0
  10. package/crates/js_to_tish/src/transform/expr.rs +610 -0
  11. package/crates/js_to_tish/src/transform/stmt.rs +503 -0
  12. package/crates/js_to_tish/src/transform.rs +60 -0
  13. package/crates/tish/Cargo.toml +54 -0
  14. package/crates/tish/src/cargo_native_registry.rs +32 -0
  15. package/crates/tish/src/cli_help.rs +565 -0
  16. package/crates/tish/src/main.rs +781 -0
  17. package/crates/tish/src/repl_completion.rs +200 -0
  18. package/crates/tish/tests/cargo_example_compile.rs +67 -0
  19. package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
  20. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
  21. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
  22. package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
  23. package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
  24. package/crates/tish/tests/integration_test.rs +1095 -0
  25. package/crates/tish/tests/run_optimize_stdout_parity.rs +50 -0
  26. package/crates/tish/tests/shortcircuit.rs +65 -0
  27. package/crates/tish_ast/Cargo.toml +9 -0
  28. package/crates/tish_ast/src/ast.rs +620 -0
  29. package/crates/tish_ast/src/lib.rs +5 -0
  30. package/crates/tish_build_utils/Cargo.toml +11 -0
  31. package/crates/tish_build_utils/src/lib.rs +577 -0
  32. package/crates/tish_builtins/Cargo.toml +20 -0
  33. package/crates/tish_builtins/src/array.rs +441 -0
  34. package/crates/tish_builtins/src/construct.rs +159 -0
  35. package/crates/tish_builtins/src/globals.rs +213 -0
  36. package/crates/tish_builtins/src/helpers.rs +35 -0
  37. package/crates/tish_builtins/src/lib.rs +16 -0
  38. package/crates/tish_builtins/src/math.rs +89 -0
  39. package/crates/tish_builtins/src/object.rs +36 -0
  40. package/crates/tish_builtins/src/string.rs +647 -0
  41. package/crates/tish_builtins/src/symbol.rs +83 -0
  42. package/crates/tish_bytecode/Cargo.toml +17 -0
  43. package/crates/tish_bytecode/src/chunk.rs +96 -0
  44. package/crates/tish_bytecode/src/compiler.rs +1760 -0
  45. package/crates/tish_bytecode/src/encoding.rs +100 -0
  46. package/crates/tish_bytecode/src/lib.rs +19 -0
  47. package/crates/tish_bytecode/src/opcode.rs +142 -0
  48. package/crates/tish_bytecode/src/peephole.rs +189 -0
  49. package/crates/tish_bytecode/src/serialize.rs +163 -0
  50. package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
  51. package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
  52. package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
  53. package/crates/tish_compile/Cargo.toml +26 -0
  54. package/crates/tish_compile/src/codegen.rs +5332 -0
  55. package/crates/tish_compile/src/infer.rs +292 -0
  56. package/crates/tish_compile/src/lib.rs +164 -0
  57. package/crates/tish_compile/src/resolve.rs +1388 -0
  58. package/crates/tish_compile/src/types.rs +501 -0
  59. package/crates/tish_compile_js/Cargo.toml +18 -0
  60. package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
  61. package/crates/tish_compile_js/src/codegen.rs +871 -0
  62. package/crates/tish_compile_js/src/error.rs +20 -0
  63. package/crates/tish_compile_js/src/lib.rs +26 -0
  64. package/crates/tish_compile_js/src/tests_jsx.rs +350 -0
  65. package/crates/tish_compiler_wasm/Cargo.toml +21 -0
  66. package/crates/tish_compiler_wasm/src/lib.rs +57 -0
  67. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +473 -0
  68. package/crates/tish_core/Cargo.toml +26 -0
  69. package/crates/tish_core/src/console_style.rs +160 -0
  70. package/crates/tish_core/src/json.rs +387 -0
  71. package/crates/tish_core/src/lib.rs +17 -0
  72. package/crates/tish_core/src/macros.rs +36 -0
  73. package/crates/tish_core/src/uri.rs +118 -0
  74. package/crates/tish_core/src/value.rs +696 -0
  75. package/crates/tish_core/src/vmref.rs +178 -0
  76. package/crates/tish_cranelift/Cargo.toml +19 -0
  77. package/crates/tish_cranelift/src/lib.rs +43 -0
  78. package/crates/tish_cranelift/src/link.rs +117 -0
  79. package/crates/tish_cranelift/src/lower.rs +85 -0
  80. package/crates/tish_cranelift_runtime/Cargo.toml +25 -0
  81. package/crates/tish_cranelift_runtime/src/lib.rs +45 -0
  82. package/crates/tish_eval/Cargo.toml +45 -0
  83. package/crates/tish_eval/src/eval.rs +3717 -0
  84. package/crates/tish_eval/src/http.rs +188 -0
  85. package/crates/tish_eval/src/lib.rs +99 -0
  86. package/crates/tish_eval/src/natives.rs +399 -0
  87. package/crates/tish_eval/src/promise.rs +179 -0
  88. package/crates/tish_eval/src/regex.rs +299 -0
  89. package/crates/tish_eval/src/timers.rs +120 -0
  90. package/crates/tish_eval/src/value.rs +318 -0
  91. package/crates/tish_eval/src/value_convert.rs +111 -0
  92. package/crates/tish_fmt/Cargo.toml +16 -0
  93. package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
  94. package/crates/tish_fmt/src/lib.rs +2101 -0
  95. package/crates/tish_jsx_web/Cargo.toml +9 -0
  96. package/crates/tish_jsx_web/README.md +5 -0
  97. package/crates/tish_jsx_web/src/lib.rs +2 -0
  98. package/crates/tish_lexer/Cargo.toml +9 -0
  99. package/crates/tish_lexer/src/lib.rs +716 -0
  100. package/crates/tish_lexer/src/token.rs +163 -0
  101. package/crates/tish_lint/Cargo.toml +18 -0
  102. package/crates/tish_lint/src/bin/tish-lint.rs +195 -0
  103. package/crates/tish_lint/src/lib.rs +289 -0
  104. package/crates/tish_llvm/Cargo.toml +13 -0
  105. package/crates/tish_llvm/src/lib.rs +115 -0
  106. package/crates/tish_lsp/Cargo.toml +25 -0
  107. package/crates/tish_lsp/README.md +26 -0
  108. package/crates/tish_lsp/src/builtin_goto.rs +362 -0
  109. package/crates/tish_lsp/src/import_goto.rs +562 -0
  110. package/crates/tish_lsp/src/main.rs +1046 -0
  111. package/crates/tish_native/Cargo.toml +16 -0
  112. package/crates/tish_native/src/build.rs +427 -0
  113. package/crates/tish_native/src/config.rs +48 -0
  114. package/crates/tish_native/src/lib.rs +416 -0
  115. package/crates/tish_opt/Cargo.toml +13 -0
  116. package/crates/tish_opt/src/lib.rs +943 -0
  117. package/crates/tish_parser/Cargo.toml +11 -0
  118. package/crates/tish_parser/src/lib.rs +332 -0
  119. package/crates/tish_parser/src/parser.rs +2304 -0
  120. package/crates/tish_pg/Cargo.toml +34 -0
  121. package/crates/tish_pg/README.md +38 -0
  122. package/crates/tish_pg/src/error.rs +52 -0
  123. package/crates/tish_pg/src/lib.rs +955 -0
  124. package/crates/tish_resolve/Cargo.toml +13 -0
  125. package/crates/tish_resolve/src/lib.rs +3561 -0
  126. package/crates/tish_resolve/src/pos.rs +141 -0
  127. package/crates/tish_runtime/Cargo.toml +96 -0
  128. package/crates/tish_runtime/src/http.rs +1298 -0
  129. package/crates/tish_runtime/src/http_fetch.rs +471 -0
  130. package/crates/tish_runtime/src/http_hyper.rs +418 -0
  131. package/crates/tish_runtime/src/http_prefork.rs +189 -0
  132. package/crates/tish_runtime/src/lib.rs +1192 -0
  133. package/crates/tish_runtime/src/native_promise.rs +15 -0
  134. package/crates/tish_runtime/src/promise.rs +248 -0
  135. package/crates/tish_runtime/src/promise_io.rs +38 -0
  136. package/crates/tish_runtime/src/timers.rs +166 -0
  137. package/crates/tish_runtime/src/ws.rs +761 -0
  138. package/crates/tish_runtime/tests/fetch_readable_stream.rs +102 -0
  139. package/crates/tish_ui/Cargo.toml +17 -0
  140. package/crates/tish_ui/src/jsx.rs +682 -0
  141. package/crates/tish_ui/src/lib.rs +20 -0
  142. package/crates/tish_ui/src/runtime/hooks.rs +569 -0
  143. package/crates/tish_ui/src/runtime/mod.rs +180 -0
  144. package/crates/tish_vm/Cargo.toml +47 -0
  145. package/crates/tish_vm/src/lib.rs +39 -0
  146. package/crates/tish_vm/src/vm.rs +2192 -0
  147. package/crates/tish_vm/tests/fixtures/or_string_cmd.tish +2 -0
  148. package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
  149. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +150 -0
  150. package/crates/tish_wasm/Cargo.toml +15 -0
  151. package/crates/tish_wasm/src/lib.rs +424 -0
  152. package/crates/tish_wasm_runtime/Cargo.toml +37 -0
  153. package/crates/tish_wasm_runtime/src/gpu.rs +413 -0
  154. package/crates/tish_wasm_runtime/src/lib.rs +42 -0
  155. package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
  156. package/crates/tishlang_cargo_bindgen/src/classify.rs +263 -0
  157. package/crates/tishlang_cargo_bindgen/src/discover.rs +125 -0
  158. package/crates/tishlang_cargo_bindgen/src/infer.rs +382 -0
  159. package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
  160. package/crates/tishlang_cargo_bindgen/src/main.rs +167 -0
  161. package/crates/tishlang_cargo_bindgen/src/metadata.rs +117 -0
  162. package/justfile +268 -0
  163. package/package.json +1 -1
  164. package/platform/darwin-arm64/tish-fmt +0 -0
@@ -0,0 +1,1298 @@
1
+ //! HTTP server + shared request parsing. Client `fetch` lives in `http_fetch.rs`.
2
+ //!
3
+ //! ## Concurrency model
4
+ //!
5
+ //! `serve(port, handler)` spawns `num_workers` OS threads (default
6
+ //! `num_cpus`, tuned via env `TISH_HTTP_WORKERS`). Each worker binds its own
7
+ //! accept socket with `SO_REUSEPORT` on Linux so the kernel load-balances
8
+ //! `accept()` across cores; on macOS we still bind N sockets with
9
+ //! `SO_REUSEPORT` (which exists but has different semantics) so each worker
10
+ //! gets its own tiny_http connection pool but the accept queue is shared at
11
+ //! the kernel level.
12
+ //!
13
+ //! Without `send-values`, the handler captures `Value::Function` backed by
14
+ //! `Rc` and is `!Send`, so handler execution stays on a single VM thread:
15
+ //!
16
+ //! ```text
17
+ //! worker 0 ──┐
18
+ //! worker 1 ──┼─> mpsc::SyncSender<Job> ──> VM thread (handler) ──> oneshot
19
+ //! worker N ──┘ │
20
+ //! worker writes response bytes (parallel) <─────────────┘
21
+ //! ```
22
+ //!
23
+ //! Parallel accept + parse + response-write while preserving the single-
24
+ //! threaded VM guarantee. Cached `Date:` header + shared `Arc<str>` response
25
+ //! bodies round out the hot-path optimisations.
26
+
27
+ use std::fs::File;
28
+ use std::io::Write;
29
+ use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
30
+ use std::sync::{Arc, OnceLock};
31
+
32
+ #[cfg(not(feature = "send-values"))]
33
+ use std::collections::VecDeque;
34
+ #[cfg(not(feature = "send-values"))]
35
+ use std::sync::mpsc;
36
+ use std::thread;
37
+ use std::time::{Duration, SystemTime, UNIX_EPOCH};
38
+
39
+ use tishlang_core::{ObjectMap, Value};
40
+ use tokio::runtime::Runtime;
41
+
42
+ thread_local! {
43
+ pub(crate) static RUNTIME: Runtime = tokio::runtime::Builder::new_multi_thread()
44
+ .worker_threads(4)
45
+ .enable_all()
46
+ .build()
47
+ .expect("Failed to create tokio runtime");
48
+ }
49
+
50
+ /// Block on a future on the HTTP runtime. Uses a dedicated thread to avoid deadlocks when
51
+ /// called from contexts that share or nest tokio runtimes (e.g. WS + HTTP both enabled).
52
+ pub(crate) fn block_on_http<F>(f: F) -> F::Output
53
+ where
54
+ F: std::future::Future + Send,
55
+ F::Output: Send,
56
+ {
57
+ std::thread::scope(|s| {
58
+ let (tx, rx) = std::sync::mpsc::channel();
59
+ s.spawn(move || {
60
+ let out = RUNTIME.with(|rt| rt.block_on(f));
61
+ let _ = tx.send(out);
62
+ });
63
+ rx.recv().expect("block_on_http thread panicked")
64
+ })
65
+ }
66
+
67
+ pub fn await_fetch(args: Vec<Value>) -> Value {
68
+ crate::promise::await_promise(crate::native_promise::fetch_promise(args))
69
+ }
70
+
71
+ pub fn await_fetch_all(args: Vec<Value>) -> Value {
72
+ crate::promise::await_promise(crate::native_promise::fetch_all_promise(args))
73
+ }
74
+
75
+ pub(crate) fn extract_method(options: Option<&Value>) -> String {
76
+ options
77
+ .and_then(|v| match v {
78
+ Value::Object(obj) => obj.borrow().strings.get("method").cloned(),
79
+ _ => None,
80
+ })
81
+ .map(|v| v.to_display_string().to_uppercase())
82
+ .unwrap_or_else(|| "GET".to_string())
83
+ }
84
+
85
+ pub(crate) fn extract_headers(options: Option<&Value>) -> Vec<(String, String)> {
86
+ options
87
+ .and_then(|v| match v {
88
+ Value::Object(obj) => obj.borrow().strings.get("headers").cloned(),
89
+ _ => None,
90
+ })
91
+ .map(|v| match v {
92
+ Value::Object(obj) => obj
93
+ .borrow()
94
+ .strings
95
+ .iter()
96
+ .map(|(k, v)| (k.to_string(), v.to_display_string()))
97
+ .collect(),
98
+ _ => vec![],
99
+ })
100
+ .unwrap_or_default()
101
+ }
102
+
103
+ pub(crate) fn extract_body(options: Option<&Value>) -> Option<String> {
104
+ options.and_then(|v| match v {
105
+ Value::Object(obj) => obj
106
+ .borrow()
107
+ .strings
108
+ .get("body")
109
+ .map(|v| v.to_display_string()),
110
+ _ => None,
111
+ })
112
+ }
113
+
114
+ pub(crate) fn build_error_response(error: &str) -> Value {
115
+ let mut obj: ObjectMap = ObjectMap::with_capacity(2);
116
+ obj.insert(Arc::from("error"), Value::String(error.into()));
117
+ obj.insert(Arc::from("ok"), Value::Bool(false));
118
+ Value::object(obj)
119
+ }
120
+
121
+ // -------- cached Date header -----------------------------------------------
122
+ //
123
+ // Lock-free via `arc-swap`: readers do `load().clone()` which is a single
124
+ // atomic fetch + ref-count inc. Writers (the 1 Hz background thread) do
125
+ // `store(new Arc)`. No Mutex contention on the 100k+ RPS path.
126
+
127
+ static DATE_HEADER: OnceLock<arc_swap::ArcSwap<String>> = OnceLock::new();
128
+
129
+ fn format_http_date(now_secs: u64) -> String {
130
+ const DAYS: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
131
+ const MONTHS: [&str; 12] = [
132
+ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
133
+ ];
134
+ let z = now_secs as i64 / 86_400;
135
+ let secs_of_day = (now_secs as i64).rem_euclid(86_400);
136
+ let h = secs_of_day / 3600;
137
+ let m = (secs_of_day % 3600) / 60;
138
+ let s = secs_of_day % 60;
139
+ let dow = ((z + 4).rem_euclid(7)) as usize;
140
+ let (y, mo, d) = civil_from_days(z);
141
+ format!(
142
+ "{}, {:02} {} {:04} {:02}:{:02}:{:02} GMT",
143
+ DAYS[dow],
144
+ d,
145
+ MONTHS[(mo - 1) as usize],
146
+ y,
147
+ h,
148
+ m,
149
+ s
150
+ )
151
+ }
152
+
153
+ fn civil_from_days(days: i64) -> (i32, u32, u32) {
154
+ let z = days + 719_468;
155
+ let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
156
+ let doe = (z - era * 146_097) as u64;
157
+ let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
158
+ let y = yoe as i64 + era * 400;
159
+ let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
160
+ let mp = (5 * doy + 2) / 153;
161
+ let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
162
+ let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
163
+ let y = if m <= 2 { y + 1 } else { y };
164
+ (y as i32, m, d)
165
+ }
166
+
167
+ fn ensure_date_thread() -> &'static arc_swap::ArcSwap<String> {
168
+ DATE_HEADER.get_or_init(|| {
169
+ let initial = Arc::new(format_http_date(
170
+ SystemTime::now()
171
+ .duration_since(UNIX_EPOCH)
172
+ .map(|d| d.as_secs())
173
+ .unwrap_or(0),
174
+ ));
175
+ let slot = arc_swap::ArcSwap::new(initial);
176
+ thread::Builder::new()
177
+ .name("tish-http-date".into())
178
+ .spawn(move || loop {
179
+ thread::sleep(Duration::from_millis(1000));
180
+ let now = SystemTime::now()
181
+ .duration_since(UNIX_EPOCH)
182
+ .map(|d| d.as_secs())
183
+ .unwrap_or(0);
184
+ let next = Arc::new(format_http_date(now));
185
+ if let Some(cell) = DATE_HEADER.get() {
186
+ cell.store(next);
187
+ }
188
+ })
189
+ .ok();
190
+ slot
191
+ })
192
+ }
193
+
194
+ /// Lock-free snapshot of the cached Date header. Callers get an `Arc<String>`
195
+ /// clone (single atomic ref-count inc).
196
+ pub fn cached_date_header_arc() -> Arc<String> {
197
+ ensure_date_thread().load_full()
198
+ }
199
+
200
+ // -------- Send-safe request/response primitives ----------------------------
201
+
202
+ #[derive(Debug, Clone)]
203
+ pub struct RequestPrimitive {
204
+ pub method: String,
205
+ pub url: String,
206
+ pub path: String,
207
+ pub query: String,
208
+ pub headers: Vec<(String, String)>,
209
+ pub body: String,
210
+ }
211
+
212
+ impl RequestPrimitive {
213
+ fn from_tiny_http(req: &mut tiny_http::Request) -> Self {
214
+ // Single-pass split of url into (path, query) so we avoid 2 extra
215
+ // String allocations per request (~300 ns at M-series load).
216
+ let url_str = req.url();
217
+ let (path, query) = match url_str.split_once('?') {
218
+ Some((p, q)) => (p.to_string(), q.to_string()),
219
+ None => (url_str.to_string(), String::new()),
220
+ };
221
+ let url = url_str.to_string();
222
+ let method = req.method().to_string();
223
+ // Fast path: GET/HEAD/OPTIONS almost never have a body. Skip the
224
+ // reader allocation + syscall unless the body is advertised.
225
+ let has_body = !matches!(method.as_str(), "GET" | "HEAD" | "OPTIONS")
226
+ && req.body_length().map(|n| n > 0).unwrap_or(true);
227
+ let mut body = String::new();
228
+ if has_body {
229
+ let _ = req.as_reader().read_to_string(&mut body);
230
+ }
231
+ let headers = req
232
+ .headers()
233
+ .iter()
234
+ .map(|h| {
235
+ (
236
+ h.field.as_str().as_str().to_string(),
237
+ h.value.as_str().to_string(),
238
+ )
239
+ })
240
+ .collect();
241
+ Self {
242
+ method,
243
+ url,
244
+ path,
245
+ query,
246
+ headers,
247
+ body,
248
+ }
249
+ }
250
+
251
+ fn into_value(self) -> Value {
252
+ // Interned keys: reuse the same Arc<str> across every request object
253
+ // (saves 6 Arc::from allocations per request on the dispatcher hot
254
+ // path). Thread-local because Value / Arc<str> are local-scope.
255
+ thread_local! {
256
+ static KEYS: RequestKeys = RequestKeys::new();
257
+ }
258
+ KEYS.with(|keys| {
259
+ let mut obj: ObjectMap = ObjectMap::with_capacity(6);
260
+ obj.insert(Arc::clone(&keys.method), Value::String(self.method.into()));
261
+ obj.insert(Arc::clone(&keys.url), Value::String(self.url.into()));
262
+ obj.insert(Arc::clone(&keys.path), Value::String(self.path.into()));
263
+ obj.insert(Arc::clone(&keys.query), Value::String(self.query.into()));
264
+ let mut h: ObjectMap = ObjectMap::with_capacity(self.headers.len());
265
+ for (k, v) in self.headers {
266
+ h.insert(Arc::from(k), Value::String(v.into()));
267
+ }
268
+ obj.insert(
269
+ Arc::clone(&keys.headers),
270
+ Value::object(h),
271
+ );
272
+ obj.insert(Arc::clone(&keys.body), Value::String(self.body.into()));
273
+ Value::object(obj)
274
+ })
275
+ }
276
+ }
277
+
278
+ struct RequestKeys {
279
+ method: Arc<str>,
280
+ url: Arc<str>,
281
+ path: Arc<str>,
282
+ query: Arc<str>,
283
+ headers: Arc<str>,
284
+ body: Arc<str>,
285
+ }
286
+
287
+ impl RequestKeys {
288
+ fn new() -> Self {
289
+ Self {
290
+ method: Arc::from("method"),
291
+ url: Arc::from("url"),
292
+ path: Arc::from("path"),
293
+ query: Arc::from("query"),
294
+ headers: Arc::from("headers"),
295
+ body: Arc::from("body"),
296
+ }
297
+ }
298
+ }
299
+
300
+ #[derive(Debug, Clone)]
301
+ pub struct ResponsePrimitive {
302
+ pub status: u16,
303
+ pub headers: Vec<(String, String)>,
304
+ pub body: ResponseBody,
305
+ }
306
+
307
+ #[derive(Debug, Clone)]
308
+ pub enum ResponseBody {
309
+ /// Text body; `Arc<str>` shares the value with Tish's `Value::String`.
310
+ Text(Arc<str>),
311
+ Bytes(Vec<u8>),
312
+ File(String),
313
+ }
314
+
315
+ impl ResponsePrimitive {
316
+ fn from_value(value: &Value) -> Self {
317
+ if let Some((status, headers, file)) = extract_file_from_response(value) {
318
+ return Self {
319
+ status,
320
+ headers,
321
+ body: ResponseBody::File(file),
322
+ };
323
+ }
324
+
325
+ let default_status = 200u16;
326
+ match value {
327
+ Value::Object(obj) => {
328
+ let obj_ref = obj.borrow();
329
+
330
+ let status = obj_ref
331
+ .strings
332
+ .get("status")
333
+ .and_then(|v| match v {
334
+ Value::Number(n) => Some(*n as u16),
335
+ _ => None,
336
+ })
337
+ .unwrap_or(default_status);
338
+
339
+ let has_error = obj_ref.strings.contains_key("error");
340
+
341
+ let body: ResponseBody = if let Some(bb) = obj_ref.strings.get("bodyBytes") {
342
+ match bb {
343
+ Value::Array(a) => {
344
+ let v: Vec<u8> = a
345
+ .borrow()
346
+ .iter()
347
+ .filter_map(|x| match x {
348
+ Value::Number(n) => Some((*n as u32 & 0xff) as u8),
349
+ _ => None,
350
+ })
351
+ .collect();
352
+ ResponseBody::Bytes(v)
353
+ }
354
+ _ => ResponseBody::Text(Arc::from(bb.to_display_string())),
355
+ }
356
+ } else if let Some(b) = obj_ref.strings.get("body") {
357
+ match b {
358
+ Value::String(s) => ResponseBody::Text(Arc::clone(&s)),
359
+ Value::Array(a) => {
360
+ let borrow = a.borrow();
361
+ if !borrow.is_empty()
362
+ && borrow.iter().all(|x| matches!(x, Value::Number(_)))
363
+ {
364
+ ResponseBody::Bytes(
365
+ borrow
366
+ .iter()
367
+ .filter_map(|x| match x {
368
+ Value::Number(n) => Some((*n as u32 & 0xff) as u8),
369
+ _ => None,
370
+ })
371
+ .collect(),
372
+ )
373
+ } else {
374
+ ResponseBody::Text(Arc::from(b.to_display_string()))
375
+ }
376
+ }
377
+ _ => ResponseBody::Text(Arc::from(b.to_display_string())),
378
+ }
379
+ } else if has_error {
380
+ ResponseBody::Text(Arc::from(
381
+ obj_ref
382
+ .strings
383
+ .get("error")
384
+ .map(|v| v.to_display_string())
385
+ .unwrap_or_default(),
386
+ ))
387
+ } else {
388
+ ResponseBody::Text(Arc::from(""))
389
+ };
390
+
391
+ let status = if has_error && status == default_status {
392
+ 500
393
+ } else {
394
+ status
395
+ };
396
+
397
+ let headers = obj_ref
398
+ .strings
399
+ .get("headers")
400
+ .and_then(|v| match v {
401
+ Value::Object(h) => Some(
402
+ h.borrow()
403
+ .strings
404
+ .iter()
405
+ .map(|(k, v)| (k.to_string(), v.to_display_string()))
406
+ .collect(),
407
+ ),
408
+ _ => None,
409
+ })
410
+ .unwrap_or_default();
411
+
412
+ Self {
413
+ status,
414
+ headers,
415
+ body,
416
+ }
417
+ }
418
+ Value::String(s) => Self {
419
+ status: default_status,
420
+ headers: vec![],
421
+ body: ResponseBody::Text(Arc::clone(s)),
422
+ },
423
+ _ => Self {
424
+ status: default_status,
425
+ headers: vec![],
426
+ body: ResponseBody::Text(Arc::from("")),
427
+ },
428
+ }
429
+ }
430
+ }
431
+
432
+ // -------- legacy shims -----------------------------------------------------
433
+
434
+ #[allow(dead_code)]
435
+ pub fn request_to_value(request: &mut tiny_http::Request) -> Value {
436
+ RequestPrimitive::from_tiny_http(request).into_value()
437
+ }
438
+
439
+ #[allow(dead_code)]
440
+ pub fn value_to_response(value: &Value) -> (u16, Vec<(String, String)>, String) {
441
+ let r = ResponsePrimitive::from_value(value);
442
+ let body = match r.body {
443
+ ResponseBody::Text(s) => s.to_string(),
444
+ ResponseBody::Bytes(b) => String::from_utf8(b).unwrap_or_default(),
445
+ ResponseBody::File(_) => String::new(),
446
+ };
447
+ (r.status, r.headers, body)
448
+ }
449
+
450
+ fn extract_file_from_response(value: &Value) -> Option<(u16, Vec<(String, String)>, String)> {
451
+ let Value::Object(obj) = value else {
452
+ return None;
453
+ };
454
+ let obj_ref = obj.borrow();
455
+ let Value::String(file_path) = obj_ref.strings.get("file")? else {
456
+ return None;
457
+ };
458
+ let file_path = file_path.to_string();
459
+ let status = obj_ref
460
+ .strings
461
+ .get("status")
462
+ .and_then(|v| match v {
463
+ Value::Number(n) => Some(*n as u16),
464
+ _ => None,
465
+ })
466
+ .unwrap_or(200);
467
+ let headers = obj_ref
468
+ .strings
469
+ .get("headers")
470
+ .and_then(|v| match v {
471
+ Value::Object(h) => Some(
472
+ h.borrow()
473
+ .strings
474
+ .iter()
475
+ .map(|(k, v)| (k.to_string(), v.to_display_string()))
476
+ .collect(),
477
+ ),
478
+ _ => None,
479
+ })
480
+ .unwrap_or_default();
481
+ Some((status, headers, file_path))
482
+ }
483
+
484
+ // -------- response writers -------------------------------------------------
485
+
486
+ fn inject_default_headers(headers: &mut Vec<(String, String)>) {
487
+ let has_date = headers.iter().any(|(k, _)| k.eq_ignore_ascii_case("Date"));
488
+ let has_server = headers
489
+ .iter()
490
+ .any(|(k, _)| k.eq_ignore_ascii_case("Server"));
491
+ if !has_date {
492
+ // One atomic load + ref-inc + one String allocation for the Vec slot.
493
+ headers.push(("Date".into(), cached_date_header_arc().as_str().to_string()));
494
+ }
495
+ if !has_server {
496
+ headers.push(("Server".into(), "Tish".into()));
497
+ }
498
+ }
499
+
500
+ #[allow(dead_code)]
501
+ pub fn send_response(
502
+ request: tiny_http::Request,
503
+ status: u16,
504
+ mut headers: Vec<(String, String)>,
505
+ body: String,
506
+ ) {
507
+ send_response_arc(request, status, headers.drain(..).collect(), Arc::from(body));
508
+ }
509
+
510
+ pub fn send_response_arc(
511
+ request: tiny_http::Request,
512
+ status: u16,
513
+ mut headers: Vec<(String, String)>,
514
+ body: Arc<str>,
515
+ ) {
516
+ inject_default_headers(&mut headers);
517
+ let status_code = tiny_http::StatusCode(status);
518
+ let len = body.len();
519
+ let bytes: Arc<[u8]> = Arc::from(body.as_bytes());
520
+ let mut response = tiny_http::Response::new(
521
+ status_code,
522
+ vec![],
523
+ ArcBytesReader::new(bytes),
524
+ Some(len),
525
+ None,
526
+ );
527
+ for (key, value) in headers {
528
+ if let Ok(header) = tiny_http::Header::from_bytes(key.as_bytes(), value.as_bytes()) {
529
+ response = response.with_header(header);
530
+ }
531
+ }
532
+ let _ = request.respond(response);
533
+ }
534
+
535
+ struct ArcBytesReader {
536
+ bytes: Arc<[u8]>,
537
+ pos: usize,
538
+ }
539
+
540
+ impl ArcBytesReader {
541
+ fn new(bytes: Arc<[u8]>) -> Self {
542
+ Self { bytes, pos: 0 }
543
+ }
544
+ }
545
+
546
+ impl std::io::Read for ArcBytesReader {
547
+ fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
548
+ let remaining = self.bytes.len().saturating_sub(self.pos);
549
+ let n = remaining.min(buf.len());
550
+ if n == 0 {
551
+ return Ok(0);
552
+ }
553
+ buf[..n].copy_from_slice(&self.bytes[self.pos..self.pos + n]);
554
+ self.pos += n;
555
+ Ok(n)
556
+ }
557
+ }
558
+
559
+ pub fn send_response_bytes(
560
+ request: tiny_http::Request,
561
+ status: u16,
562
+ mut headers: Vec<(String, String)>,
563
+ body: Vec<u8>,
564
+ ) {
565
+ inject_default_headers(&mut headers);
566
+ let status_code = tiny_http::StatusCode(status);
567
+ let len = body.len();
568
+ let mut response = tiny_http::Response::new(
569
+ status_code,
570
+ vec![],
571
+ std::io::Cursor::new(body),
572
+ Some(len),
573
+ None,
574
+ );
575
+ for (key, value) in headers {
576
+ if let Ok(header) = tiny_http::Header::from_bytes(key.as_bytes(), value.as_bytes()) {
577
+ response = response.with_header(header);
578
+ }
579
+ }
580
+ let _ = request.respond(response);
581
+ }
582
+
583
+ fn send_file_response(
584
+ request: tiny_http::Request,
585
+ status: u16,
586
+ mut headers: Vec<(String, String)>,
587
+ file_path: String,
588
+ ) {
589
+ inject_default_headers(&mut headers);
590
+ let file = match File::open(&file_path) {
591
+ Ok(f) => f,
592
+ Err(e) => {
593
+ eprintln!("Failed to open file {}: {}", file_path, e);
594
+ let fallback =
595
+ tiny_http::Response::from_string(format!("File not found: {}", file_path))
596
+ .with_status_code(tiny_http::StatusCode(500));
597
+ let _ = request.respond(fallback);
598
+ return;
599
+ }
600
+ };
601
+ let status_code = tiny_http::StatusCode(status);
602
+ let mut response = tiny_http::Response::from_file(file).with_status_code(status_code);
603
+ for (key, value) in headers {
604
+ if let Ok(header) = tiny_http::Header::from_bytes(key.as_bytes(), value.as_bytes()) {
605
+ response = response.with_header(header);
606
+ }
607
+ }
608
+ let _ = request.respond(response);
609
+ }
610
+
611
+ fn respond_from_primitive(request: tiny_http::Request, resp: ResponsePrimitive) {
612
+ match resp.body {
613
+ ResponseBody::Text(s) => send_response_arc(request, resp.status, resp.headers, s),
614
+ ResponseBody::Bytes(b) => send_response_bytes(request, resp.status, resp.headers, b),
615
+ ResponseBody::File(p) => send_file_response(request, resp.status, resp.headers, p),
616
+ }
617
+ }
618
+
619
+ // -------- static-route fast path ------------------------------------------
620
+ //
621
+ // Endpoints whose body is constant (TFB `/plaintext` and `/json`) don't need
622
+ // to roundtrip through the Tish VM per request. `register_static_route`
623
+ // stores the pre-built response bytes in a process-wide table; workers
624
+ // consult the table before pushing to the dispatcher and serve the
625
+ // response directly, skipping Value construction and channel hops entirely.
626
+ //
627
+ // This is the main lever that lets N workers actually *scale* for plaintext:
628
+ // without it, every request serialises through the single VM thread.
629
+
630
+ #[derive(Clone)]
631
+ struct StaticRoute {
632
+ body: Arc<[u8]>,
633
+ content_type: Arc<str>,
634
+ }
635
+
636
+ type StaticRoutes = std::collections::HashMap<String, StaticRoute>;
637
+
638
+ /// Lock-free static-route map: readers use `load()` which is a single atomic
639
+ /// operation. Writers (registration) do `rcu` to publish a new Arc. This is
640
+ /// what lets per-worker /plaintext lookups actually scale — a Mutex here was
641
+ /// bouncing cache lines between every worker thread on every request.
642
+ static STATIC_ROUTES: OnceLock<arc_swap::ArcSwap<StaticRoutes>> = OnceLock::new();
643
+
644
+ fn static_routes() -> &'static arc_swap::ArcSwap<StaticRoutes> {
645
+ STATIC_ROUTES
646
+ .get_or_init(|| arc_swap::ArcSwap::new(Arc::new(StaticRoutes::new())))
647
+ }
648
+
649
+ /// Register a static response for `path`. Subsequent requests to exactly
650
+ /// that path (ignoring query string) are served by HTTP workers directly,
651
+ /// bypassing the Tish VM dispatcher. Content-type and Server/Date are
652
+ /// appended automatically.
653
+ pub fn register_static_route(path: &str, body: &[u8], content_type: &str) {
654
+ let cell = static_routes();
655
+ cell.rcu(|cur| {
656
+ let mut next = (**cur).clone();
657
+ next.insert(
658
+ path.to_string(),
659
+ StaticRoute {
660
+ body: Arc::from(body),
661
+ content_type: Arc::from(content_type),
662
+ },
663
+ );
664
+ Arc::new(next)
665
+ });
666
+ }
667
+
668
+ fn lookup_static_route(path: &str) -> Option<StaticRoute> {
669
+ // Strip query string in-place (no allocation).
670
+ let pure = path.split('?').next().unwrap_or(path);
671
+ let guard = static_routes().load();
672
+ guard.get(pure).cloned()
673
+ }
674
+
675
+ fn serve_static_route(request: tiny_http::Request, route: StaticRoute) {
676
+ let status_code = tiny_http::StatusCode(200);
677
+ let body_len = route.body.len();
678
+ let mut response = tiny_http::Response::new(
679
+ status_code,
680
+ vec![],
681
+ ArcBytesReader::new(route.body),
682
+ Some(body_len),
683
+ None,
684
+ );
685
+ // Hand-built headers; Date comes from the cached slot.
686
+ if let Ok(h) =
687
+ tiny_http::Header::from_bytes(b"Content-Type".as_slice(), route.content_type.as_bytes())
688
+ {
689
+ response = response.with_header(h);
690
+ }
691
+ if let Ok(h) = tiny_http::Header::from_bytes(b"Server".as_slice(), b"Tish".as_slice()) {
692
+ response = response.with_header(h);
693
+ }
694
+ let date = cached_date_header_arc();
695
+ if let Ok(h) = tiny_http::Header::from_bytes(b"Date".as_slice(), date.as_bytes()) {
696
+ response = response.with_header(h);
697
+ }
698
+ let _ = request.respond(response);
699
+ }
700
+
701
+ // -------- SO_REUSEPORT listeners -------------------------------------------
702
+
703
+ #[cfg(all(unix, not(any(target_os = "solaris", target_os = "illumos"))))]
704
+ fn set_reuse_port(s: &socket2::Socket) {
705
+ let _ = s.set_reuse_port(true);
706
+ }
707
+
708
+ fn bind_listeners(port: u16, n: usize) -> Result<Vec<std::net::TcpListener>, String> {
709
+ use socket2::{Domain, Protocol, SockAddr, Socket, Type};
710
+
711
+ let addr: std::net::SocketAddr = format!("0.0.0.0:{}", port)
712
+ .parse()
713
+ .map_err(|e: std::net::AddrParseError| e.to_string())?;
714
+ let sa: SockAddr = addr.into();
715
+
716
+ let mut out = Vec::with_capacity(n);
717
+ for _ in 0..n {
718
+ let s = Socket::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP))
719
+ .map_err(|e| format!("socket: {}", e))?;
720
+ s.set_reuse_address(true)
721
+ .map_err(|e| format!("set_reuse_address: {}", e))?;
722
+ #[cfg(all(unix, not(any(target_os = "solaris", target_os = "illumos"))))]
723
+ {
724
+ set_reuse_port(&s);
725
+ }
726
+ s.set_nodelay(true).ok();
727
+ s.bind(&sa).map_err(|e| format!("bind {}: {}", port, e))?;
728
+ s.listen(1024).map_err(|e| format!("listen: {}", e))?;
729
+ out.push(s.into());
730
+ }
731
+ Ok(out)
732
+ }
733
+
734
+ fn num_workers() -> usize {
735
+ if let Ok(v) = std::env::var("TISH_HTTP_WORKERS") {
736
+ if let Ok(n) = v.parse::<usize>() {
737
+ if n >= 1 {
738
+ return n;
739
+ }
740
+ }
741
+ }
742
+ std::thread::available_parallelism()
743
+ .map(|n| n.get())
744
+ .unwrap_or(1)
745
+ }
746
+
747
+ /// Target number of prefork worker **processes**. Distinct from
748
+ /// `num_workers`, which is the number of OS *threads* per process.
749
+ ///
750
+ /// In prefork mode every process runs one accept thread, so this is the
751
+ /// knob that actually controls parallelism. Defaults to the number of
752
+ /// logical CPUs; `TISH_PREFORK_WORKERS=N` or `TISH_HTTP_WORKERS=N` overrides.
753
+ pub(crate) fn num_prefork_workers() -> usize {
754
+ if let Ok(v) = std::env::var("TISH_PREFORK_WORKERS") {
755
+ if let Ok(n) = v.parse::<usize>() {
756
+ if n >= 1 {
757
+ return n;
758
+ }
759
+ }
760
+ }
761
+ // Accept TISH_HTTP_WORKERS too so users don't have to learn a new var
762
+ // just to turn on prefork — the docs advertise a single env var.
763
+ if let Ok(v) = std::env::var("TISH_HTTP_WORKERS") {
764
+ if let Ok(n) = v.parse::<usize>() {
765
+ if n >= 1 {
766
+ return n;
767
+ }
768
+ }
769
+ }
770
+ std::thread::available_parallelism()
771
+ .map(|n| n.get())
772
+ .unwrap_or(1)
773
+ }
774
+
775
+ // -------- serve() ----------------------------------------------------------
776
+
777
+ /// Start an HTTP server that handles requests using the provided handler function.
778
+ ///
779
+ /// When `send-values` is enabled (required for the `http` feature) the
780
+ /// handler is `Fn + Send + Sync`, so we run it directly on each HTTP
781
+ /// accept thread instead of funneling every request through a single
782
+ /// dispatcher. That's the multi-core unlock for the VM: handler calls
783
+ /// execute in parallel across N OS threads, with `Value` safely shared
784
+ /// via `VmRef` (`Arc<Mutex>`) on every array/object.
785
+ ///
786
+ /// On builds without `send-values` (wasm / single-threaded targets)
787
+ /// the handler only needs `Fn`; we fall back to the mpsc-dispatch model
788
+ /// where all handler calls execute on one VM thread.
789
+ #[cfg(feature = "send-values")]
790
+ pub fn serve<F>(args: &[Value], handler: F) -> Value
791
+ where
792
+ F: Fn(&[Value]) -> Value + Send + Sync + 'static,
793
+ {
794
+ serve_impl_with_factory(args, None, Some(Arc::new(handler)))
795
+ }
796
+
797
+ /// `serve(port, { onWorker, workers? })` — each accept thread calls the
798
+ /// factory once to build **its own** handler closure. This is the pattern
799
+ /// for per-worker state (DB connections, caches, counters) with the same
800
+ /// `Value: Send + Sync` parallelism as [`serve`]. The factory is invoked
801
+ /// with a single `Value::Number(worker_id)` arg and must return a
802
+ /// `Value::Function`. Panics at startup if the factory returns anything
803
+ /// else.
804
+ #[cfg(feature = "send-values")]
805
+ pub fn serve_per_worker<FF>(args: &[Value], factory: FF) -> Value
806
+ where
807
+ FF: Fn(usize) -> Arc<dyn Fn(&[Value]) -> Value + Send + Sync> + Send + Sync + 'static,
808
+ {
809
+ serve_impl_with_factory(
810
+ args,
811
+ Some(Arc::new(factory)
812
+ as Arc<dyn Fn(usize) -> Arc<dyn Fn(&[Value]) -> Value + Send + Sync> + Send + Sync>),
813
+ None,
814
+ )
815
+ }
816
+
817
+ #[cfg(not(feature = "send-values"))]
818
+ pub fn serve<F>(args: &[Value], handler: F) -> Value
819
+ where
820
+ F: Fn(&[Value]) -> Value,
821
+ {
822
+ serve_impl(args, handler)
823
+ }
824
+
825
+ /// Shared implementation for [`serve`] (single handler) and
826
+ /// [`serve_per_worker`] (per-worker factory). Exactly one of
827
+ /// `factory` / `shared_handler` is provided; the other is `None`.
828
+ #[cfg(feature = "send-values")]
829
+ fn serve_impl_with_factory(
830
+ args: &[Value],
831
+ factory: Option<
832
+ Arc<dyn Fn(usize) -> Arc<dyn Fn(&[Value]) -> Value + Send + Sync> + Send + Sync>,
833
+ >,
834
+ shared_handler: Option<Arc<dyn Fn(&[Value]) -> Value + Send + Sync>>,
835
+ ) -> Value {
836
+ debug_assert!(factory.is_some() ^ shared_handler.is_some());
837
+ let port = match args.first() {
838
+ Some(Value::Number(n)) => *n as u16,
839
+ _ => return build_error_response("serve requires a port number"),
840
+ };
841
+
842
+ let max_requests: Option<usize> = args.get(2).and_then(|v| match v {
843
+ Value::Number(n) if *n >= 1.0 => Some(*n as usize),
844
+ _ => None,
845
+ });
846
+
847
+ ensure_date_thread();
848
+
849
+ // --- prefork: spawn one subprocess per extra core --------------------
850
+ //
851
+ // This is the big multi-core unlock for Tish HTTP: because the Tish VM
852
+ // runs on `Rc`/`RefCell` (`Value` is `!Send`), we can't run multiple
853
+ // handler threads in one process. Instead, we fork the process — each
854
+ // child re-execs the same binary and runs its own single-threaded VM.
855
+ // All N processes `SO_REUSEPORT`-bind the same port and the kernel
856
+ // load-balances connections across them. Same pattern as nginx,
857
+ // gunicorn, puma cluster, phpfpm.
858
+ //
859
+ // * Parent (role = Parent) spawns N-1 children and keeps its own accept
860
+ // loop running (so we don't waste a core just coordinating).
861
+ // * Children (role = Child(id)) just run their accept loop.
862
+ // * Single (role = Single) skips forking — explicit opt-out, or we're
863
+ // already inside a child.
864
+ let role = crate::http_prefork::role_from_env();
865
+ let prefork_n = num_prefork_workers();
866
+ let mut children = Vec::new();
867
+ let mut prefork_stop: Option<Arc<AtomicBool>> = None;
868
+ match role {
869
+ crate::http_prefork::PreforkRole::Parent if prefork_n > 1 => {
870
+ match crate::http_prefork::spawn_children(prefork_n) {
871
+ Ok(c) => {
872
+ let handles = crate::http_prefork::install_parent_signal_handler(c);
873
+ prefork_stop = Some(handles);
874
+ // Children are spawned; fall through and run our own
875
+ // accept loop as worker 0.
876
+ children.push(()); // sentinel: we're in a prefork group
877
+ }
878
+ Err(e) => {
879
+ eprintln!(
880
+ "[tish http] prefork spawn failed, continuing as single process: {}",
881
+ e
882
+ );
883
+ }
884
+ }
885
+ }
886
+ _ => {}
887
+ }
888
+
889
+ // Per-process accept threads. In prefork mode this is 1; SO_REUSEPORT
890
+ // between processes provides kernel-level load balancing. Without
891
+ // prefork we fall back to the old multi-thread-in-one-process layout.
892
+ let workers = if children.is_empty() {
893
+ num_workers()
894
+ } else {
895
+ 1
896
+ };
897
+ let listeners = match bind_listeners(port, workers) {
898
+ Ok(l) => l,
899
+ Err(e) => {
900
+ eprintln!("[tish http] failed to bind: {}", e);
901
+ return build_error_response(&e);
902
+ }
903
+ };
904
+
905
+ let worker_id = match role {
906
+ crate::http_prefork::PreforkRole::Child(id) => id,
907
+ _ => 0,
908
+ };
909
+ if matches!(role, crate::http_prefork::PreforkRole::Parent) && !children.is_empty() {
910
+ println!(
911
+ "tish http: listening on http://0.0.0.0:{} ({} process{} x {} accept thread{})",
912
+ port,
913
+ prefork_n,
914
+ if prefork_n == 1 { "" } else { "es" },
915
+ workers,
916
+ if workers == 1 { "" } else { "s" }
917
+ );
918
+ } else if worker_id == 0 {
919
+ println!(
920
+ "tish http: listening on http://0.0.0.0:{} ({} worker{})",
921
+ port,
922
+ workers,
923
+ if workers == 1 { "" } else { "s" }
924
+ );
925
+ }
926
+
927
+ let stop = prefork_stop.clone().unwrap_or_else(|| Arc::new(AtomicBool::new(false)));
928
+
929
+ if max_requests == Some(1) {
930
+ let p = port;
931
+ thread::spawn(move || {
932
+ thread::sleep(Duration::from_millis(50));
933
+ if let Ok(mut s) = std::net::TcpStream::connect(format!("127.0.0.1:{}", p)) {
934
+ let _ =
935
+ s.write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n");
936
+ let _ = s.shutdown(std::net::Shutdown::Write);
937
+ }
938
+ });
939
+ }
940
+
941
+ // Per-thread handler: either a single shared `Arc<dyn Fn>` or one
942
+ // built per worker via the user-supplied factory. Because
943
+ // `Value: Send + Sync` under `send-values`, both variants run in
944
+ // parallel on N accept threads without a dispatcher queue.
945
+ //
946
+ // The worker index passed to the factory is *global across prefork
947
+ // processes*: parent is 0..N-1, each child is its own id, so a 14-
948
+ // core + 14-process layout produces worker ids 0..13 total, never
949
+ // duplicated.
950
+ let global_worker_base = match role {
951
+ crate::http_prefork::PreforkRole::Child(id) => id * workers,
952
+ _ => 0,
953
+ };
954
+
955
+ let processed = Arc::new(AtomicUsize::new(0));
956
+ let mut worker_handles = Vec::with_capacity(workers);
957
+ for (idx, listener) in listeners.into_iter().enumerate() {
958
+ let stop = Arc::clone(&stop);
959
+ let processed = Arc::clone(&processed);
960
+ let max = max_requests;
961
+ let worker_handler: Arc<dyn Fn(&[Value]) -> Value + Send + Sync> =
962
+ if let Some(f) = &factory {
963
+ f(global_worker_base + idx)
964
+ } else {
965
+ Arc::clone(shared_handler.as_ref().unwrap())
966
+ };
967
+ let handle = thread::Builder::new()
968
+ .name(format!("tish-http-w{}", idx))
969
+ .spawn(move || worker_loop_direct(listener, worker_handler, stop, processed, max))
970
+ .expect("spawn tish-http worker");
971
+ worker_handles.push(handle);
972
+ }
973
+
974
+ // Wait until one of: stop flag set, a worker bumps `processed >= max`, or
975
+ // we're shutting down (parent received SIGINT).
976
+ loop {
977
+ if stop.load(Ordering::Relaxed) {
978
+ break;
979
+ }
980
+ if let Some(m) = max_requests {
981
+ if processed.load(Ordering::Relaxed) >= m {
982
+ stop.store(true, Ordering::Relaxed);
983
+ break;
984
+ }
985
+ }
986
+ thread::sleep(Duration::from_millis(50));
987
+ }
988
+
989
+ // Kick each accept so its blocking `recv_timeout` wakes up promptly.
990
+ for _ in 0..worker_handles.len() {
991
+ let _ = std::net::TcpStream::connect(format!("127.0.0.1:{}", port));
992
+ }
993
+ for h in worker_handles {
994
+ let _ = h.join();
995
+ }
996
+
997
+ Value::Null
998
+ }
999
+
1000
+ #[cfg(not(feature = "send-values"))]
1001
+ type Job = (RequestPrimitive, mpsc::SyncSender<ResponsePrimitive>);
1002
+
1003
+ #[cfg(not(feature = "send-values"))]
1004
+ fn serve_impl<F>(args: &[Value], handler: F) -> Value
1005
+ where
1006
+ F: Fn(&[Value]) -> Value,
1007
+ {
1008
+ // Single-threaded dispatch path: multiple accept threads push onto a
1009
+ // shared `mpsc::sync_channel`, one VM thread (this one) drains and runs
1010
+ // the handler. Used by wasm / single-threaded Rust builds where
1011
+ // `Value` is `Rc`-backed and not `Send`.
1012
+ let port = match args.first() {
1013
+ Some(Value::Number(n)) => *n as u16,
1014
+ _ => return build_error_response("serve requires a port number"),
1015
+ };
1016
+ let max_requests: Option<usize> = args.get(2).and_then(|v| match v {
1017
+ Value::Number(n) if *n >= 1.0 => Some(*n as usize),
1018
+ _ => None,
1019
+ });
1020
+ ensure_date_thread();
1021
+ let workers = num_workers();
1022
+ let listeners = match bind_listeners(port, workers) {
1023
+ Ok(l) => l,
1024
+ Err(e) => {
1025
+ eprintln!("[tish http] failed to bind: {}", e);
1026
+ return build_error_response(&e);
1027
+ }
1028
+ };
1029
+ println!(
1030
+ "tish http: listening on http://0.0.0.0:{} ({} worker{}, single-vm)",
1031
+ port,
1032
+ workers,
1033
+ if workers == 1 { "" } else { "s" }
1034
+ );
1035
+
1036
+ let stop = Arc::new(AtomicBool::new(false));
1037
+ let (tx, rx) = mpsc::sync_channel::<Job>(workers * 256);
1038
+
1039
+ if max_requests == Some(1) {
1040
+ let p = port;
1041
+ thread::spawn(move || {
1042
+ thread::sleep(Duration::from_millis(50));
1043
+ if let Ok(mut s) = std::net::TcpStream::connect(format!("127.0.0.1:{}", p)) {
1044
+ let _ =
1045
+ s.write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n");
1046
+ let _ = s.shutdown(std::net::Shutdown::Write);
1047
+ }
1048
+ });
1049
+ }
1050
+
1051
+ let mut worker_handles = Vec::with_capacity(workers);
1052
+ for (idx, listener) in listeners.into_iter().enumerate() {
1053
+ let tx = tx.clone();
1054
+ let stop = Arc::clone(&stop);
1055
+ let handle = thread::Builder::new()
1056
+ .name(format!("tish-http-w{}", idx))
1057
+ .spawn(move || worker_loop(listener, tx, stop))
1058
+ .expect("spawn tish-http worker");
1059
+ worker_handles.push(handle);
1060
+ }
1061
+ drop(tx);
1062
+
1063
+ let mut count = 0usize;
1064
+ while let Ok((req_prim, resp_tx)) = rx.recv() {
1065
+ let req_value = req_prim.into_value();
1066
+ let response_value = handler(&[req_value]);
1067
+ let resp_prim = ResponsePrimitive::from_value(&response_value);
1068
+ let _ = resp_tx.send(resp_prim);
1069
+
1070
+ count += 1;
1071
+ if max_requests.map(|m| count >= m).unwrap_or(false) {
1072
+ stop.store(true, Ordering::Relaxed);
1073
+ break;
1074
+ }
1075
+ }
1076
+
1077
+ stop.store(true, Ordering::Relaxed);
1078
+ for _ in 0..worker_handles.len() {
1079
+ let _ = std::net::TcpStream::connect(format!("127.0.0.1:{}", port));
1080
+ }
1081
+ for h in worker_handles {
1082
+ let _ = h.join();
1083
+ }
1084
+
1085
+ Value::Null
1086
+ }
1087
+
1088
+ /// Parallel accept + dispatch loop used when `send-values` is on. The
1089
+ /// handler runs on the same OS thread that accepted the connection, so
1090
+ /// there is no cross-thread queue on the hot path. Static-route fast path
1091
+ /// is unchanged.
1092
+ #[cfg(feature = "send-values")]
1093
+ fn worker_loop_direct(
1094
+ listener: std::net::TcpListener,
1095
+ handler: Arc<dyn Fn(&[Value]) -> Value + Send + Sync>,
1096
+ stop: Arc<AtomicBool>,
1097
+ processed: Arc<AtomicUsize>,
1098
+ max_requests: Option<usize>,
1099
+ ) {
1100
+ let server = match tiny_http::Server::from_listener(listener, None) {
1101
+ Ok(s) => s,
1102
+ Err(e) => {
1103
+ eprintln!("[tish http] worker failed to adopt listener: {}", e);
1104
+ return;
1105
+ }
1106
+ };
1107
+ loop {
1108
+ if stop.load(Ordering::Relaxed) {
1109
+ break;
1110
+ }
1111
+ match server.recv_timeout(Duration::from_millis(250)) {
1112
+ Ok(Some(mut request)) => {
1113
+ // Static-route fast path: serve pre-baked bytes without
1114
+ // touching the Tish VM at all.
1115
+ if let Some(route) = lookup_static_route(request.url()) {
1116
+ serve_static_route(request, route);
1117
+ } else {
1118
+ let req_prim = RequestPrimitive::from_tiny_http(&mut request);
1119
+ let req_value = req_prim.into_value();
1120
+ let response_value = handler(&[req_value]);
1121
+ let resp_prim = ResponsePrimitive::from_value(&response_value);
1122
+ respond_from_primitive(request, resp_prim);
1123
+ }
1124
+ if let Some(m) = max_requests {
1125
+ let p = processed.fetch_add(1, Ordering::Relaxed) + 1;
1126
+ if p >= m {
1127
+ stop.store(true, Ordering::Relaxed);
1128
+ break;
1129
+ }
1130
+ }
1131
+ }
1132
+ Ok(None) => {} // timeout; re-check stop flag
1133
+ Err(_) => break,
1134
+ }
1135
+ }
1136
+ }
1137
+
1138
+ #[cfg(not(feature = "send-values"))]
1139
+ fn worker_loop(
1140
+ listener: std::net::TcpListener,
1141
+ dispatch: mpsc::SyncSender<Job>,
1142
+ stop: Arc<AtomicBool>,
1143
+ ) {
1144
+ let server = match tiny_http::Server::from_listener(listener, None) {
1145
+ Ok(s) => s,
1146
+ Err(e) => {
1147
+ eprintln!("[tish http] worker failed to adopt listener: {}", e);
1148
+ return;
1149
+ }
1150
+ };
1151
+
1152
+ // Per-worker buffer of in-flight requests. We interleave accept with
1153
+ // response drain so a slow VM-thread handler doesn't block new accepts
1154
+ // (up to the pending capacity) and so single-request responses still
1155
+ // flush immediately when accept() blocks on the next connection.
1156
+ let mut pending: VecDeque<(tiny_http::Request, mpsc::Receiver<ResponsePrimitive>)> =
1157
+ VecDeque::with_capacity(256);
1158
+
1159
+ loop {
1160
+ if stop.load(Ordering::Relaxed) {
1161
+ break;
1162
+ }
1163
+
1164
+ // Short-poll accept so we can drain pending responses even when
1165
+ // there's no new request arriving.
1166
+ match server.recv_timeout(Duration::from_millis(1)) {
1167
+ Ok(Some(mut request)) => {
1168
+ // Static-route fast path: serve pre-baked bytes without a
1169
+ // round trip through the VM dispatcher. This is what lets
1170
+ // per-worker accept actually scale for /plaintext + /json.
1171
+ if let Some(route) = lookup_static_route(request.url()) {
1172
+ serve_static_route(request, route);
1173
+ continue;
1174
+ }
1175
+ let req_prim = RequestPrimitive::from_tiny_http(&mut request);
1176
+ let (resp_tx, resp_rx) = mpsc::sync_channel::<ResponsePrimitive>(1);
1177
+ if dispatch.send((req_prim, resp_tx)).is_err() {
1178
+ break;
1179
+ }
1180
+ pending.push_back((request, resp_rx));
1181
+ }
1182
+ Ok(None) => {
1183
+ // timed out; fall through to drain
1184
+ }
1185
+ Err(_) => break,
1186
+ }
1187
+
1188
+ // Drain all ready responses (FIFO preserves order).
1189
+ while let Some((_, rx)) = pending.front() {
1190
+ match rx.try_recv() {
1191
+ Ok(resp) => {
1192
+ let (req, _rx) = pending.pop_front().unwrap();
1193
+ respond_from_primitive(req, resp);
1194
+ }
1195
+ Err(mpsc::TryRecvError::Empty) => break,
1196
+ Err(mpsc::TryRecvError::Disconnected) => {
1197
+ pending.pop_front();
1198
+ }
1199
+ }
1200
+ }
1201
+ }
1202
+
1203
+ while let Some((req, rx)) = pending.pop_front() {
1204
+ match rx.recv_timeout(Duration::from_millis(500)) {
1205
+ Ok(resp) => respond_from_primitive(req, resp),
1206
+ Err(_) => drop(req),
1207
+ }
1208
+ }
1209
+ }
1210
+
1211
+ // -------- public shims for alternate HTTP backends ------------------------
1212
+ //
1213
+ // Exposed so `http_hyper.rs` (and any future backend) can reuse the same
1214
+ // request / response primitives, static-route table, cached Date header,
1215
+ // SO_REUSEPORT binder, and worker count. Keeping these in one place
1216
+ // guarantees parity between tiny_http and hyper code paths — cached Date,
1217
+ // Server header, static-route table, key interning all work identically.
1218
+
1219
+ #[cfg(feature = "http-hyper")]
1220
+ impl RequestPrimitive {
1221
+ /// Build a `RequestPrimitive` from already-parsed parts. Used by
1222
+ /// backends that do their own HTTP parsing (e.g. hyper).
1223
+ pub fn new_pub(
1224
+ method: String,
1225
+ url: String,
1226
+ path: String,
1227
+ query: String,
1228
+ headers: Vec<(String, String)>,
1229
+ body: String,
1230
+ ) -> Self {
1231
+ Self {
1232
+ method,
1233
+ url,
1234
+ path,
1235
+ query,
1236
+ headers,
1237
+ body,
1238
+ }
1239
+ }
1240
+
1241
+ /// Same as the crate-private `into_value` but re-exported for
1242
+ /// alternate HTTP backends.
1243
+ pub fn into_value_pub(self) -> Value {
1244
+ self.into_value()
1245
+ }
1246
+ }
1247
+
1248
+ #[cfg(feature = "http-hyper")]
1249
+ impl ResponsePrimitive {
1250
+ /// Public alias of the crate-private `from_value`. Used by alternate
1251
+ /// HTTP backends (hyper) so Tish handlers return the same response
1252
+ /// shape regardless of which server is underneath.
1253
+ pub fn from_value_pub(value: &Value) -> Self {
1254
+ Self::from_value(value)
1255
+ }
1256
+ }
1257
+
1258
+ /// Public snapshot of a static route (body + content-type), returned to
1259
+ /// alternate HTTP backends so they can serve pre-baked responses without
1260
+ /// reaching into our private types.
1261
+ #[cfg(feature = "http-hyper")]
1262
+ #[derive(Clone)]
1263
+ pub struct StaticRouteSnapshot {
1264
+ pub body: Arc<[u8]>,
1265
+ pub content_type: Arc<str>,
1266
+ }
1267
+
1268
+ #[cfg(feature = "http-hyper")]
1269
+ pub fn lookup_static_route_pub(path: &str) -> Option<StaticRouteSnapshot> {
1270
+ lookup_static_route(path).map(|r| StaticRouteSnapshot {
1271
+ body: r.body,
1272
+ content_type: r.content_type,
1273
+ })
1274
+ }
1275
+
1276
+ #[cfg(feature = "http-hyper")]
1277
+ pub fn num_workers_pub() -> usize {
1278
+ num_workers()
1279
+ }
1280
+
1281
+ #[cfg(feature = "http-hyper")]
1282
+ pub fn num_prefork_workers_pub() -> usize {
1283
+ num_prefork_workers()
1284
+ }
1285
+
1286
+ #[cfg(feature = "http-hyper")]
1287
+ pub fn bind_listeners_reuseport(
1288
+ port: u16,
1289
+ n: usize,
1290
+ ) -> Result<Vec<std::net::TcpListener>, String> {
1291
+ bind_listeners(port, n)
1292
+ }
1293
+
1294
+ #[cfg(feature = "http-hyper")]
1295
+ pub fn build_error_response_pub(msg: &str) -> Value {
1296
+ build_error_response(msg)
1297
+ }
1298
+