@tishlang/tish-format 1.0.12 → 2.0.1

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 (189) hide show
  1. package/Cargo.toml +51 -0
  2. package/LICENSE +13 -0
  3. package/bin/tish-format +0 -0
  4. package/crates/js_to_tish/Cargo.toml +11 -0
  5. package/crates/js_to_tish/README.md +18 -0
  6. package/crates/js_to_tish/src/error.rs +55 -0
  7. package/crates/js_to_tish/src/lib.rs +11 -0
  8. package/crates/js_to_tish/src/span_util.rs +35 -0
  9. package/crates/js_to_tish/src/transform/expr.rs +611 -0
  10. package/crates/js_to_tish/src/transform/stmt.rs +503 -0
  11. package/crates/js_to_tish/src/transform.rs +60 -0
  12. package/crates/tish/Cargo.toml +62 -0
  13. package/crates/tish/build.rs +21 -0
  14. package/crates/tish/src/cargo_native_registry.rs +32 -0
  15. package/crates/tish/src/cli_help.rs +576 -0
  16. package/crates/tish/src/main.rs +853 -0
  17. package/crates/tish/src/repl_completion.rs +199 -0
  18. package/crates/tish/tests/cargo_example_compile.rs +67 -0
  19. package/crates/tish/tests/error_source_location.rs +36 -0
  20. package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
  21. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
  22. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
  23. package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
  24. package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
  25. package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
  26. package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
  27. package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
  28. package/crates/tish/tests/integration_test.rs +1406 -0
  29. package/crates/tish/tests/run_optimize_stdout_parity.rs +50 -0
  30. package/crates/tish/tests/shortcircuit.rs +65 -0
  31. package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
  32. package/crates/tish/tests/tty_capability.rs +43 -0
  33. package/crates/tish_ast/Cargo.toml +9 -0
  34. package/crates/tish_ast/src/ast.rs +649 -0
  35. package/crates/tish_ast/src/lib.rs +5 -0
  36. package/crates/tish_build_utils/Cargo.toml +11 -0
  37. package/crates/tish_build_utils/src/lib.rs +577 -0
  38. package/crates/tish_builtins/Cargo.toml +22 -0
  39. package/crates/tish_builtins/src/array.rs +803 -0
  40. package/crates/tish_builtins/src/collections.rs +481 -0
  41. package/crates/tish_builtins/src/construct.rs +199 -0
  42. package/crates/tish_builtins/src/date.rs +538 -0
  43. package/crates/tish_builtins/src/globals.rs +293 -0
  44. package/crates/tish_builtins/src/helpers.rs +35 -0
  45. package/crates/tish_builtins/src/iterator.rs +129 -0
  46. package/crates/tish_builtins/src/lib.rs +21 -0
  47. package/crates/tish_builtins/src/math.rs +89 -0
  48. package/crates/tish_builtins/src/number.rs +96 -0
  49. package/crates/tish_builtins/src/object.rs +36 -0
  50. package/crates/tish_builtins/src/string.rs +646 -0
  51. package/crates/tish_builtins/src/symbol.rs +83 -0
  52. package/crates/tish_builtins/src/typedarrays.rs +298 -0
  53. package/crates/tish_bytecode/Cargo.toml +17 -0
  54. package/crates/tish_bytecode/src/chunk.rs +164 -0
  55. package/crates/tish_bytecode/src/compiler.rs +2604 -0
  56. package/crates/tish_bytecode/src/encoding.rs +102 -0
  57. package/crates/tish_bytecode/src/lib.rs +20 -0
  58. package/crates/tish_bytecode/src/opcode.rs +185 -0
  59. package/crates/tish_bytecode/src/peephole.rs +189 -0
  60. package/crates/tish_bytecode/src/serialize.rs +193 -0
  61. package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
  62. package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
  63. package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
  64. package/crates/tish_compile/Cargo.toml +27 -0
  65. package/crates/tish_compile/src/check.rs +774 -0
  66. package/crates/tish_compile/src/codegen.rs +7317 -0
  67. package/crates/tish_compile/src/infer.rs +1681 -0
  68. package/crates/tish_compile/src/lib.rs +206 -0
  69. package/crates/tish_compile/src/resolve.rs +1951 -0
  70. package/crates/tish_compile/src/types.rs +605 -0
  71. package/crates/tish_compile_js/Cargo.toml +18 -0
  72. package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
  73. package/crates/tish_compile_js/src/codegen.rs +938 -0
  74. package/crates/tish_compile_js/src/error.rs +20 -0
  75. package/crates/tish_compile_js/src/lib.rs +26 -0
  76. package/crates/tish_compile_js/src/tests_jsx.rs +414 -0
  77. package/crates/tish_compiler_wasm/Cargo.toml +21 -0
  78. package/crates/tish_compiler_wasm/src/lib.rs +57 -0
  79. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +473 -0
  80. package/crates/tish_core/Cargo.toml +32 -0
  81. package/crates/tish_core/src/console_style.rs +170 -0
  82. package/crates/tish_core/src/json.rs +430 -0
  83. package/crates/tish_core/src/lib.rs +20 -0
  84. package/crates/tish_core/src/macros.rs +36 -0
  85. package/crates/tish_core/src/shape.rs +85 -0
  86. package/crates/tish_core/src/uri.rs +118 -0
  87. package/crates/tish_core/src/value.rs +1350 -0
  88. package/crates/tish_core/src/vmref.rs +183 -0
  89. package/crates/tish_cranelift/Cargo.toml +19 -0
  90. package/crates/tish_cranelift/src/lib.rs +43 -0
  91. package/crates/tish_cranelift/src/link.rs +130 -0
  92. package/crates/tish_cranelift/src/lower.rs +85 -0
  93. package/crates/tish_cranelift_runtime/Cargo.toml +26 -0
  94. package/crates/tish_cranelift_runtime/src/lib.rs +45 -0
  95. package/crates/tish_eval/Cargo.toml +51 -0
  96. package/crates/tish_eval/src/eval.rs +4265 -0
  97. package/crates/tish_eval/src/http.rs +191 -0
  98. package/crates/tish_eval/src/lib.rs +99 -0
  99. package/crates/tish_eval/src/natives.rs +551 -0
  100. package/crates/tish_eval/src/promise.rs +179 -0
  101. package/crates/tish_eval/src/regex.rs +299 -0
  102. package/crates/tish_eval/src/timers.rs +120 -0
  103. package/crates/tish_eval/src/value.rs +336 -0
  104. package/crates/tish_eval/src/value_convert.rs +117 -0
  105. package/crates/tish_ffi/Cargo.toml +26 -0
  106. package/crates/tish_ffi/src/lib.rs +518 -0
  107. package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
  108. package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
  109. package/crates/tish_ffi/tests/loader.rs +65 -0
  110. package/crates/tish_fmt/Cargo.toml +16 -0
  111. package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
  112. package/crates/tish_fmt/src/lib.rs +2157 -0
  113. package/crates/tish_jsx_web/Cargo.toml +9 -0
  114. package/crates/tish_jsx_web/README.md +5 -0
  115. package/crates/tish_jsx_web/src/lib.rs +2 -0
  116. package/crates/tish_lexer/Cargo.toml +9 -0
  117. package/crates/tish_lexer/src/lib.rs +1104 -0
  118. package/crates/tish_lexer/src/token.rs +170 -0
  119. package/crates/tish_lint/Cargo.toml +18 -0
  120. package/crates/tish_lint/src/bin/tish-lint.rs +195 -0
  121. package/crates/tish_lint/src/lib.rs +281 -0
  122. package/crates/tish_llvm/Cargo.toml +13 -0
  123. package/crates/tish_llvm/src/lib.rs +115 -0
  124. package/crates/tish_lsp/Cargo.toml +25 -0
  125. package/crates/tish_lsp/README.md +26 -0
  126. package/crates/tish_lsp/src/builtin_goto.rs +362 -0
  127. package/crates/tish_lsp/src/import_goto.rs +564 -0
  128. package/crates/tish_lsp/src/main.rs +1459 -0
  129. package/crates/tish_native/Cargo.toml +16 -0
  130. package/crates/tish_native/src/build.rs +481 -0
  131. package/crates/tish_native/src/config.rs +48 -0
  132. package/crates/tish_native/src/lib.rs +416 -0
  133. package/crates/tish_opt/Cargo.toml +13 -0
  134. package/crates/tish_opt/src/lib.rs +1046 -0
  135. package/crates/tish_parser/Cargo.toml +11 -0
  136. package/crates/tish_parser/src/lib.rs +386 -0
  137. package/crates/tish_parser/src/parser.rs +2726 -0
  138. package/crates/tish_pg/Cargo.toml +34 -0
  139. package/crates/tish_pg/README.md +38 -0
  140. package/crates/tish_pg/src/error.rs +52 -0
  141. package/crates/tish_pg/src/lib.rs +955 -0
  142. package/crates/tish_resolve/Cargo.toml +13 -0
  143. package/crates/tish_resolve/src/lib.rs +3601 -0
  144. package/crates/tish_resolve/src/pos.rs +141 -0
  145. package/crates/tish_runtime/Cargo.toml +100 -0
  146. package/crates/tish_runtime/src/http.rs +1347 -0
  147. package/crates/tish_runtime/src/http_fetch.rs +492 -0
  148. package/crates/tish_runtime/src/http_hyper.rs +441 -0
  149. package/crates/tish_runtime/src/http_prefork.rs +189 -0
  150. package/crates/tish_runtime/src/lib.rs +1447 -0
  151. package/crates/tish_runtime/src/native_promise.rs +15 -0
  152. package/crates/tish_runtime/src/promise.rs +558 -0
  153. package/crates/tish_runtime/src/promise_io.rs +38 -0
  154. package/crates/tish_runtime/src/timers.rs +172 -0
  155. package/crates/tish_runtime/src/tty.rs +226 -0
  156. package/crates/tish_runtime/src/ws.rs +778 -0
  157. package/crates/tish_runtime/tests/fetch_readable_stream.rs +102 -0
  158. package/crates/tish_ui/Cargo.toml +17 -0
  159. package/crates/tish_ui/src/jsx.rs +692 -0
  160. package/crates/tish_ui/src/lib.rs +20 -0
  161. package/crates/tish_ui/src/runtime/hooks.rs +573 -0
  162. package/crates/tish_ui/src/runtime/mod.rs +183 -0
  163. package/crates/tish_vm/Cargo.toml +60 -0
  164. package/crates/tish_vm/src/jit.rs +1050 -0
  165. package/crates/tish_vm/src/lib.rs +41 -0
  166. package/crates/tish_vm/src/vm.rs +3536 -0
  167. package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
  168. package/crates/tish_vm/tests/fixtures/or_string_cmd.tish +2 -0
  169. package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
  170. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +150 -0
  171. package/crates/tish_wasm/Cargo.toml +15 -0
  172. package/crates/tish_wasm/src/lib.rs +428 -0
  173. package/crates/tish_wasm_runtime/Cargo.toml +37 -0
  174. package/crates/tish_wasm_runtime/src/gpu.rs +429 -0
  175. package/crates/tish_wasm_runtime/src/lib.rs +42 -0
  176. package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
  177. package/crates/tishlang_cargo_bindgen/src/classify.rs +261 -0
  178. package/crates/tishlang_cargo_bindgen/src/discover.rs +125 -0
  179. package/crates/tishlang_cargo_bindgen/src/infer.rs +382 -0
  180. package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
  181. package/crates/tishlang_cargo_bindgen/src/main.rs +167 -0
  182. package/crates/tishlang_cargo_bindgen/src/metadata.rs +117 -0
  183. package/justfile +276 -0
  184. package/package.json +2 -2
  185. package/platform/darwin-arm64/tish-fmt +0 -0
  186. package/platform/darwin-x64/tish-fmt +0 -0
  187. package/platform/linux-arm64/tish-fmt +0 -0
  188. package/platform/linux-x64/tish-fmt +0 -0
  189. package/platform/win32-x64/tish-fmt.exe +0 -0
@@ -0,0 +1,492 @@
1
+ //! Web Fetch–aligned Response, ReadableStream, reader.read(), text()/json().
2
+
3
+ use std::pin::Pin;
4
+ use std::sync::{Arc, Mutex};
5
+
6
+ use tishlang_core::VmRef;
7
+
8
+ use bytes::Bytes;
9
+ use futures::Stream;
10
+ use futures::StreamExt;
11
+ use tishlang_core::{NativeFn, ObjectMap, TishOpaque, TishPromise, Value};
12
+
13
+ use crate::http::{build_error_response, extract_body, extract_headers, extract_method};
14
+
15
+ // --- Promises (Send payloads only; Value built on awaiting thread) ---
16
+
17
+ struct FetchResponsePromise {
18
+ rx: Mutex<Option<tokio::sync::oneshot::Receiver<Result<reqwest::Response, String>>>>,
19
+ }
20
+
21
+ impl TishPromise for FetchResponsePromise {
22
+ fn block_until_settled(&self) -> std::result::Result<Value, Value> {
23
+ let rx = self.rx.lock().unwrap().take();
24
+ if let Some(rx) = rx {
25
+ let r = crate::http::block_on_http(rx);
26
+ match r {
27
+ Ok(Ok(resp)) => Ok(response_value_from_reqwest(resp)),
28
+ Ok(Err(e)) => Ok(build_error_response(&e)),
29
+ Err(_) => Err(Value::String("Promise dropped".into())),
30
+ }
31
+ } else {
32
+ Err(Value::String("Promise already consumed".into()))
33
+ }
34
+ }
35
+ }
36
+
37
+ struct FetchAllResponsesPromise {
38
+ #[allow(clippy::type_complexity)]
39
+ rx: Mutex<
40
+ Option<
41
+ tokio::sync::oneshot::Receiver<Result<Vec<Result<reqwest::Response, String>>, String>>,
42
+ >,
43
+ >,
44
+ }
45
+
46
+ impl TishPromise for FetchAllResponsesPromise {
47
+ fn block_until_settled(&self) -> std::result::Result<Value, Value> {
48
+ let rx = self.rx.lock().unwrap().take();
49
+ if let Some(rx) = rx {
50
+ let r = crate::http::block_on_http(rx);
51
+ match r {
52
+ Ok(Ok(vec)) => {
53
+ let out: Vec<Value> = vec
54
+ .into_iter()
55
+ .map(|x| {
56
+ x.map(response_value_from_reqwest)
57
+ .unwrap_or_else(|e| build_error_response(&e))
58
+ })
59
+ .collect();
60
+ Ok(Value::Array(VmRef::new(out)))
61
+ }
62
+ Ok(Err(e)) => Ok(build_error_response(&e)),
63
+ Err(_) => Err(Value::String("Promise dropped".into())),
64
+ }
65
+ } else {
66
+ Err(Value::String("Promise already consumed".into()))
67
+ }
68
+ }
69
+ }
70
+
71
+ enum ReadChunk {
72
+ Done,
73
+ Bytes(Vec<u8>),
74
+ }
75
+
76
+ struct ReadChunkPromise {
77
+ rx: Mutex<Option<tokio::sync::oneshot::Receiver<Result<ReadChunk, String>>>>,
78
+ }
79
+
80
+ impl TishPromise for ReadChunkPromise {
81
+ fn block_until_settled(&self) -> std::result::Result<Value, Value> {
82
+ let rx = self.rx.lock().unwrap().take();
83
+ if let Some(rx) = rx {
84
+ let r = crate::http::block_on_http(rx);
85
+ match r {
86
+ Ok(Ok(ReadChunk::Done)) => {
87
+ let mut o = ObjectMap::default();
88
+ o.insert(Arc::from("done"), Value::Bool(true));
89
+ o.insert(Arc::from("value"), Value::Null);
90
+ Ok(Value::object(o))
91
+ }
92
+ Ok(Ok(ReadChunk::Bytes(b))) => {
93
+ let arr: Vec<Value> = b.iter().map(|u| Value::Number(*u as f64)).collect();
94
+ let mut o = ObjectMap::default();
95
+ o.insert(Arc::from("done"), Value::Bool(false));
96
+ o.insert(Arc::from("value"), Value::Array(VmRef::new(arr)));
97
+ Ok(Value::object(o))
98
+ }
99
+ Ok(Err(e)) => Err({
100
+ let mut obj = ObjectMap::default();
101
+ obj.insert(Arc::from("error"), Value::String(e.into()));
102
+ Value::object(obj)
103
+ }),
104
+ Err(_) => Err(Value::String("Promise dropped".into())),
105
+ }
106
+ } else {
107
+ Err(Value::String("Promise already consumed".into()))
108
+ }
109
+ }
110
+ }
111
+
112
+ struct JsonTextPromise {
113
+ rx: Mutex<Option<tokio::sync::oneshot::Receiver<Result<String, String>>>>,
114
+ }
115
+
116
+ impl TishPromise for JsonTextPromise {
117
+ fn block_until_settled(&self) -> std::result::Result<Value, Value> {
118
+ let rx = self.rx.lock().unwrap().take();
119
+ if let Some(rx) = rx {
120
+ let r = crate::http::block_on_http(rx);
121
+ match r {
122
+ Ok(Ok(s)) => match tishlang_core::json_parse(&s) {
123
+ Ok(v) => Ok(v),
124
+ Err(e) => Err({
125
+ let mut obj = ObjectMap::default();
126
+ obj.insert(Arc::from("error"), Value::String(e.into()));
127
+ Value::object(obj)
128
+ }),
129
+ },
130
+ Ok(Err(e)) => Err({
131
+ let mut obj = ObjectMap::default();
132
+ obj.insert(Arc::from("error"), Value::String(e.into()));
133
+ Value::object(obj)
134
+ }),
135
+ Err(_) => Err(Value::String("Promise dropped".into())),
136
+ }
137
+ } else {
138
+ Err(Value::String("Promise already consumed".into()))
139
+ }
140
+ }
141
+ }
142
+
143
+ // --- Body ---
144
+
145
+ pub struct HttpBody {
146
+ state: Mutex<BodyState>,
147
+ }
148
+
149
+ enum BodyState {
150
+ Fresh(Option<reqwest::Response>),
151
+ ReadInProgress,
152
+ Gone,
153
+ }
154
+
155
+ impl HttpBody {
156
+ pub fn new(response: reqwest::Response) -> Self {
157
+ Self {
158
+ state: Mutex::new(BodyState::Fresh(Some(response))),
159
+ }
160
+ }
161
+
162
+ #[allow(clippy::type_complexity)]
163
+ fn take_stream(
164
+ &self,
165
+ ) -> Result<Pin<Box<dyn Stream<Item = Result<Bytes, reqwest::Error>> + Send>>, String> {
166
+ let mut g = self.state.lock().unwrap();
167
+ match &mut *g {
168
+ BodyState::Fresh(r) => {
169
+ let resp = r
170
+ .take()
171
+ .ok_or_else(|| "Response body already consumed".to_string())?;
172
+ *g = BodyState::ReadInProgress;
173
+ Ok(Box::pin(resp.bytes_stream()))
174
+ }
175
+ BodyState::ReadInProgress => {
176
+ Err("ReadableStream is locked; getReader() already called".into())
177
+ }
178
+ BodyState::Gone => Err("Response body already consumed".into()),
179
+ }
180
+ }
181
+
182
+ pub fn take_text_async(
183
+ &self,
184
+ ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String, String>> + Send + '_>>
185
+ {
186
+ let resp = {
187
+ let mut g = self.state.lock().unwrap();
188
+ match &mut *g {
189
+ BodyState::Fresh(r) => match r.take() {
190
+ Some(resp) => {
191
+ *g = BodyState::Gone;
192
+ Ok(resp)
193
+ }
194
+ None => Err("Response body already consumed".into()),
195
+ },
196
+ BodyState::ReadInProgress => {
197
+ Err("Cannot call text(): body is locked by ReadableStreamDefaultReader".into())
198
+ }
199
+ BodyState::Gone => Err("Response body already consumed".into()),
200
+ }
201
+ };
202
+ Box::pin(async move {
203
+ match resp {
204
+ Ok(r) => r.text().await.map_err(|e| e.to_string()),
205
+ Err(e) => Err(e),
206
+ }
207
+ })
208
+ }
209
+
210
+ fn mark_gone_after_stream(&self) {
211
+ let mut g = self.state.lock().unwrap();
212
+ *g = BodyState::Gone;
213
+ }
214
+ }
215
+
216
+ pub struct HttpReadableStream {
217
+ body: Arc<HttpBody>,
218
+ }
219
+
220
+ impl TishOpaque for HttpReadableStream {
221
+ fn type_name(&self) -> &'static str {
222
+ "ReadableStream"
223
+ }
224
+
225
+ fn as_any(&self) -> &dyn std::any::Any {
226
+ self
227
+ }
228
+
229
+ fn get_method(&self, name: &str) -> Option<NativeFn> {
230
+ if name != "getReader" {
231
+ return None;
232
+ }
233
+ let body = Arc::clone(&self.body);
234
+ Some(tishlang_core::native_fn(move |_args: &[Value]| match body.take_stream() {
235
+ Ok(stream) => {
236
+ let inner = Arc::new(tokio::sync::Mutex::new(StreamSlot { stream }));
237
+ Value::Opaque(Arc::new(HttpStreamReader {
238
+ inner,
239
+ body: Arc::clone(&body),
240
+ }))
241
+ }
242
+ Err(e) => {
243
+ let mut m = ObjectMap::default();
244
+ m.insert(Arc::from("error"), Value::String(e.into()));
245
+ Value::object(m)
246
+ }
247
+ }))
248
+ }
249
+ }
250
+
251
+ struct StreamSlot {
252
+ stream: Pin<Box<dyn Stream<Item = Result<Bytes, reqwest::Error>> + Send>>,
253
+ }
254
+
255
+ pub struct HttpStreamReader {
256
+ inner: Arc<tokio::sync::Mutex<StreamSlot>>,
257
+ body: Arc<HttpBody>,
258
+ }
259
+
260
+ impl TishOpaque for HttpStreamReader {
261
+ fn type_name(&self) -> &'static str {
262
+ "ReadableStreamDefaultReader"
263
+ }
264
+
265
+ fn as_any(&self) -> &dyn std::any::Any {
266
+ self
267
+ }
268
+
269
+ fn get_method(&self, name: &str) -> Option<NativeFn> {
270
+ if name != "read" {
271
+ return None;
272
+ }
273
+ let inner = Arc::clone(&self.inner);
274
+ let body = Arc::clone(&self.body);
275
+ Some(tishlang_core::native_fn(move |_args: &[Value]| {
276
+ let inner = Arc::clone(&inner);
277
+ let body = Arc::clone(&body);
278
+ let (tx, rx) = tokio::sync::oneshot::channel();
279
+ crate::http::RUNTIME.with(|rt| {
280
+ rt.spawn(async move {
281
+ let mut slot = inner.lock().await;
282
+ match slot.stream.next().await {
283
+ None => {
284
+ body.mark_gone_after_stream();
285
+ let _ = tx.send(Ok(ReadChunk::Done));
286
+ }
287
+ Some(Ok(b)) => {
288
+ let _ = tx.send(Ok(ReadChunk::Bytes(b.to_vec())));
289
+ }
290
+ Some(Err(e)) => {
291
+ let _ = tx.send(Err(e.to_string()));
292
+ }
293
+ }
294
+ });
295
+ });
296
+ Value::Promise(Arc::new(ReadChunkPromise {
297
+ rx: Mutex::new(Some(rx)),
298
+ }))
299
+ }))
300
+ }
301
+ }
302
+
303
+ fn headers_to_value(headers: &reqwest::header::HeaderMap) -> Value {
304
+ let mut headers_obj: ObjectMap = ObjectMap::with_capacity(headers.len());
305
+ for (key, value) in headers.iter() {
306
+ if let Ok(v) = value.to_str() {
307
+ headers_obj.insert(Arc::from(key.as_str()), Value::String(v.into()));
308
+ }
309
+ }
310
+ Value::object(headers_obj)
311
+ }
312
+
313
+ pub fn response_value_from_reqwest(response: reqwest::Response) -> Value {
314
+ let status = response.status().as_u16() as f64;
315
+ let ok = response.status().is_success();
316
+ let headers_val = headers_to_value(response.headers());
317
+ let body_holder = Arc::new(HttpBody::new(response));
318
+ let stream = HttpReadableStream {
319
+ body: Arc::clone(&body_holder),
320
+ };
321
+ let body_stream_val = Value::Opaque(Arc::new(stream));
322
+ let bh_text = Arc::clone(&body_holder);
323
+ let bh_json = Arc::clone(&body_holder);
324
+ let text_fn: NativeFn = tishlang_core::native_fn(move |_args: &[Value]| {
325
+ let bh = Arc::clone(&bh_text);
326
+ let (tx, rx) = tokio::sync::oneshot::channel();
327
+ crate::http::RUNTIME.with(|rt| {
328
+ rt.spawn(async move {
329
+ let r = bh.take_text_async().await;
330
+ let _ = tx.send(r);
331
+ });
332
+ });
333
+ crate::promise_io::string_result_promise(rx)
334
+ });
335
+ let json_fn: NativeFn = tishlang_core::native_fn(move |_args: &[Value]| {
336
+ let bh = Arc::clone(&bh_json);
337
+ let (tx, rx) = tokio::sync::oneshot::channel();
338
+ crate::http::RUNTIME.with(|rt| {
339
+ rt.spawn(async move {
340
+ let r = bh.take_text_async().await;
341
+ let _ = tx.send(r);
342
+ });
343
+ });
344
+ Value::Promise(Arc::new(JsonTextPromise {
345
+ rx: Mutex::new(Some(rx)),
346
+ }))
347
+ });
348
+ let mut obj: ObjectMap = ObjectMap::default();
349
+ obj.insert(Arc::from("status"), Value::Number(status));
350
+ obj.insert(Arc::from("ok"), Value::Bool(ok));
351
+ obj.insert(Arc::from("headers"), headers_val);
352
+ obj.insert(Arc::from("body"), body_stream_val);
353
+ obj.insert(Arc::from("text"), Value::Function(text_fn));
354
+ obj.insert(Arc::from("json"), Value::Function(json_fn));
355
+ Value::object(obj)
356
+ }
357
+
358
+ /// Shared, hardened outbound HTTP client: request + connect timeouts (a no-timeout fetch is
359
+ /// an outbound resource-pinning / slowloris vector) and a bounded redirect chain. Cached so
360
+ /// the connection pool is reused across requests.
361
+ /// NOTE: this does NOT yet block internal/metadata IPs — full SSRF defense (deny loopback /
362
+ /// link-local / RFC1918 after DNS resolution) is a follow-up that needs a policy decision.
363
+ fn fetch_client() -> &'static reqwest::Client {
364
+ use std::sync::OnceLock;
365
+ static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
366
+ CLIENT.get_or_init(|| {
367
+ reqwest::Client::builder()
368
+ .timeout(std::time::Duration::from_secs(30))
369
+ .connect_timeout(std::time::Duration::from_secs(10))
370
+ .redirect(reqwest::redirect::Policy::limited(5))
371
+ .build()
372
+ .unwrap_or_else(|_| reqwest::Client::new())
373
+ })
374
+ }
375
+
376
+ async fn send_request_parts(
377
+ url: String,
378
+ method: String,
379
+ headers: Vec<(String, String)>,
380
+ body: Option<String>,
381
+ ) -> Result<reqwest::Response, String> {
382
+ let client = fetch_client();
383
+ let mut req = match method.as_str() {
384
+ "GET" => client.get(&url),
385
+ "POST" => client.post(&url),
386
+ "PUT" => client.put(&url),
387
+ "DELETE" => client.delete(&url),
388
+ "PATCH" => client.patch(&url),
389
+ "HEAD" => client.head(&url),
390
+ _ => client.get(&url),
391
+ };
392
+ for (key, value) in headers {
393
+ req = req.header(key, value);
394
+ }
395
+ if let Some(body) = body {
396
+ req = req.body(body);
397
+ }
398
+ req.send().await.map_err(|e| e.to_string())
399
+ }
400
+
401
+ pub fn fetch_promise_from_args(args: Vec<Value>) -> Value {
402
+ let url = match args.first() {
403
+ Some(Value::String(s)) => s.to_string(),
404
+ Some(v) => v.to_display_string(),
405
+ None => {
406
+ let (tx, rx) = tokio::sync::oneshot::channel();
407
+ let _ = tx.send(Err("fetch requires a URL".into()));
408
+ return Value::Promise(Arc::new(FetchResponsePromise {
409
+ rx: Mutex::new(Some(rx)),
410
+ }));
411
+ }
412
+ };
413
+ let method = extract_method(args.get(1));
414
+ let headers = extract_headers(args.get(1));
415
+ let body = extract_body(args.get(1));
416
+ let (tx, rx) = tokio::sync::oneshot::channel();
417
+ crate::http::RUNTIME.with(|rt| {
418
+ rt.spawn(async move {
419
+ let r = send_request_parts(url, method, headers, body).await;
420
+ let _ = tx.send(r);
421
+ });
422
+ });
423
+ Value::Promise(Arc::new(FetchResponsePromise {
424
+ rx: Mutex::new(Some(rx)),
425
+ }))
426
+ }
427
+
428
+ pub fn fetch_all_promise_from_args(args: Vec<Value>) -> Value {
429
+ let requests = match args.first() {
430
+ Some(Value::Array(arr)) => arr.borrow().clone(),
431
+ _ => {
432
+ let (tx, rx) = tokio::sync::oneshot::channel();
433
+ let _ = tx.send(Err("fetchAll requires an array of request objects".into()));
434
+ return Value::Promise(Arc::new(FetchAllResponsesPromise {
435
+ rx: Mutex::new(Some(rx)),
436
+ }));
437
+ }
438
+ };
439
+ #[allow(clippy::type_complexity)]
440
+ let mut parts: Vec<(String, String, Vec<(String, String)>, Option<String>)> = Vec::new();
441
+ for req in requests {
442
+ let (url, opt) = match &req {
443
+ Value::String(s) => (s.to_string(), None),
444
+ Value::Object(obj) => {
445
+ let obj_ref = obj.borrow();
446
+ match obj_ref
447
+ .strings
448
+ .get("url")
449
+ .map(|v| v.to_display_string())
450
+ {
451
+ Some(u) => (u, Some(req.clone())),
452
+ None => {
453
+ let (tx, rx) = tokio::sync::oneshot::channel();
454
+ let _ =
455
+ tx.send(Err("Each request object must have a 'url' property".into()));
456
+ return Value::Promise(Arc::new(FetchAllResponsesPromise {
457
+ rx: Mutex::new(Some(rx)),
458
+ }));
459
+ }
460
+ }
461
+ }
462
+ _ => {
463
+ let (tx, rx) = tokio::sync::oneshot::channel();
464
+ let _ = tx.send(Err(
465
+ "Each request must be a string URL or request object".into()
466
+ ));
467
+ return Value::Promise(Arc::new(FetchAllResponsesPromise {
468
+ rx: Mutex::new(Some(rx)),
469
+ }));
470
+ }
471
+ };
472
+ let method = extract_method(opt.as_ref());
473
+ let headers = extract_headers(opt.as_ref());
474
+ let body = extract_body(opt.as_ref());
475
+ parts.push((url, method, headers, body));
476
+ }
477
+ let (tx, rx) = tokio::sync::oneshot::channel();
478
+ crate::http::RUNTIME.with(|rt| {
479
+ rt.spawn(async move {
480
+ let futs: Vec<_> = parts
481
+ .into_iter()
482
+ .map(|(url, m, h, b)| send_request_parts(url, m, h, b))
483
+ .collect();
484
+ let results = futures::future::join_all(futs).await;
485
+ let mapped: Vec<Result<reqwest::Response, String>> = results.into_iter().collect();
486
+ let _ = tx.send(Ok(mapped));
487
+ });
488
+ });
489
+ Value::Promise(Arc::new(FetchAllResponsesPromise {
490
+ rx: Mutex::new(Some(rx)),
491
+ }))
492
+ }