@tishlang/tish-format 2.16.13 → 2.36.0
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/js_to_tish/src/transform/expr.rs +12 -2
- package/crates/tish/tests/fixtures/fs_parity_callbacks.tish +22 -0
- package/crates/tish/tests/fixtures/fs_parity_promises.tish +21 -0
- package/crates/tish/tests/fixtures/fs_parity_sync.tish +20 -0
- package/crates/tish/tests/fixtures/process_capture.tish +16 -0
- package/crates/tish/tests/fs_parity.rs +103 -0
- package/crates/tish/tests/process_capture.rs +40 -0
- package/crates/tish/tests/shortcircuit.rs +3 -3
- package/crates/tish_ast/src/ast.rs +77 -3
- package/crates/tish_builtins/Cargo.toml +1 -0
- package/crates/tish_builtins/src/array.rs +195 -16
- package/crates/tish_builtins/src/collections.rs +37 -2
- package/crates/tish_builtins/src/globals.rs +215 -25
- package/crates/tish_builtins/src/math.rs +65 -7
- package/crates/tish_builtins/src/number.rs +173 -0
- package/crates/tish_builtins/src/string.rs +168 -15
- package/crates/tish_bytecode/src/compiler.rs +56 -4
- package/crates/tish_bytecode/src/opcode.rs +4 -2
- package/crates/tish_compile/src/codegen.rs +519 -46
- package/crates/tish_compile/src/infer.rs +26 -11
- package/crates/tish_compile/src/resolve.rs +87 -10
- package/crates/tish_compile_js/src/codegen.rs +82 -24
- package/crates/tish_compile_js/src/tests_jsx.rs +27 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +17 -2
- package/crates/tish_core/src/json.rs +5 -1
- package/crates/tish_core/src/lib.rs +48 -2
- package/crates/tish_core/src/uri.rs +45 -0
- package/crates/tish_core/src/value.rs +6 -0
- package/crates/tish_eval/Cargo.toml +2 -1
- package/crates/tish_eval/src/eval.rs +661 -27
- package/crates/tish_eval/src/natives.rs +318 -18
- package/crates/tish_eval/src/regex.rs +47 -1
- package/crates/tish_fmt/Cargo.toml +1 -1
- package/crates/tish_fmt/src/lib.rs +23 -4
- package/crates/tish_opt/src/lib.rs +20 -1
- package/crates/tish_parser/src/lib.rs +41 -0
- package/crates/tish_parser/src/parser.rs +180 -11
- package/crates/tish_resolve/src/lib.rs +66 -5
- package/crates/tish_runtime/Cargo.toml +5 -1
- package/crates/tish_runtime/src/fs_ext.rs +359 -0
- package/crates/tish_runtime/src/http_hyper.rs +41 -18
- package/crates/tish_runtime/src/lib.rs +236 -10
- package/crates/tish_ui/src/jsx.rs +18 -3
- package/crates/tish_vm/src/jit.rs +1 -1
- package/crates/tish_vm/src/vm.rs +294 -60
- 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,359 @@
|
|
|
1
|
+
//! Node-compatible `fs` surface for `tish:fs` and the async `tish:fs/promises` module
|
|
2
|
+
//! (issue #122). Each operation has a single `*_core` returning `Result<Value, Value>`
|
|
3
|
+
//! (Ok = result, Err = an error value); a macro derives the synchronous export (returns the
|
|
4
|
+
//! value, or the error object on failure — tish's sync convention) and the promise export
|
|
5
|
+
//! (a fulfilled/rejected Promise — Node's `fs/promises` convention).
|
|
6
|
+
//!
|
|
7
|
+
//! Node names are primary (`readFileSync`, `statSync`, …); the existing tish names
|
|
8
|
+
//! (`readFile`, `readDir`, `fileExists`, `isDir`) are kept as aliases in the backends.
|
|
9
|
+
#![cfg(feature = "fs")]
|
|
10
|
+
|
|
11
|
+
use crate::promise::{promise_reject, promise_resolve};
|
|
12
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
13
|
+
use tishlang_builtins::helpers::make_error_value;
|
|
14
|
+
use tishlang_core::{ObjectMap, Value, VmRef};
|
|
15
|
+
|
|
16
|
+
fn path_arg(args: &[Value], i: usize) -> String {
|
|
17
|
+
args.get(i).map(|v| v.to_display_string()).unwrap_or_default()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// Map an io::Error to the same error value `read_file` already produces.
|
|
21
|
+
fn io_err(e: std::io::Error) -> Value {
|
|
22
|
+
make_error_value(e)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fn unwrap(r: Result<Value, Value>) -> Value {
|
|
26
|
+
match r {
|
|
27
|
+
Ok(v) => v,
|
|
28
|
+
Err(e) => e,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fn settle(r: Result<Value, Value>) -> Value {
|
|
33
|
+
match r {
|
|
34
|
+
Ok(v) => promise_resolve(&[v]),
|
|
35
|
+
Err(e) => promise_reject(&[e]),
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Node callback form: the trailing arg is `(err, result) => …`. Run the op on the remaining
|
|
40
|
+
/// args, invoke the callback with `(null, value)` on success or `(err, null)` on failure, and
|
|
41
|
+
/// return null. The op is synchronous under the hood, so the callback fires synchronously.
|
|
42
|
+
fn run_callback(core: fn(&[Value]) -> Result<Value, Value>, args: &[Value]) -> Value {
|
|
43
|
+
let cb = args.last().cloned().unwrap_or(Value::Null);
|
|
44
|
+
let op_args: &[Value] = if args.len() > 1 { &args[..args.len() - 1] } else { &[] };
|
|
45
|
+
let (err, data) = match core(op_args) {
|
|
46
|
+
Ok(v) => (Value::Null, v),
|
|
47
|
+
Err(e) => (e, Value::Null),
|
|
48
|
+
};
|
|
49
|
+
tishlang_core::value_call(&cb, &[err, data]);
|
|
50
|
+
Value::Null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// Generate the sync export (dual: a trailing function arg switches to the Node callback form)
|
|
54
|
+
/// and the promise export over a `*_core`.
|
|
55
|
+
macro_rules! fs_method {
|
|
56
|
+
($sync:ident, $promise:ident, $core:ident) => {
|
|
57
|
+
pub fn $sync(args: &[Value]) -> Value {
|
|
58
|
+
if let Some(Value::Function(_)) = args.last() {
|
|
59
|
+
return run_callback($core, args);
|
|
60
|
+
}
|
|
61
|
+
unwrap($core(args))
|
|
62
|
+
}
|
|
63
|
+
pub fn $promise(args: &[Value]) -> Value {
|
|
64
|
+
settle($core(args))
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fn ms_since_epoch(t: std::io::Result<SystemTime>) -> f64 {
|
|
70
|
+
t.ok()
|
|
71
|
+
.and_then(|st| st.duration_since(UNIX_EPOCH).ok())
|
|
72
|
+
.map(|d| d.as_secs_f64() * 1000.0)
|
|
73
|
+
.unwrap_or(0.0)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Build a Node-like `Stats` object from metadata (predicate methods + size + times + mode).
|
|
77
|
+
fn stats_object(md: &std::fs::Metadata) -> Value {
|
|
78
|
+
let mut m = ObjectMap::default();
|
|
79
|
+
let is_file = md.is_file();
|
|
80
|
+
let is_dir = md.is_dir();
|
|
81
|
+
let is_symlink = md.file_type().is_symlink();
|
|
82
|
+
m.insert("isFile".into(), Value::native(move |_| Value::Bool(is_file)));
|
|
83
|
+
m.insert("isDirectory".into(), Value::native(move |_| Value::Bool(is_dir)));
|
|
84
|
+
m.insert("isSymbolicLink".into(), Value::native(move |_| Value::Bool(is_symlink)));
|
|
85
|
+
m.insert("isBlockDevice".into(), Value::native(|_| Value::Bool(false)));
|
|
86
|
+
m.insert("isCharacterDevice".into(), Value::native(|_| Value::Bool(false)));
|
|
87
|
+
m.insert("isFIFO".into(), Value::native(|_| Value::Bool(false)));
|
|
88
|
+
m.insert("isSocket".into(), Value::native(|_| Value::Bool(false)));
|
|
89
|
+
m.insert("size".into(), Value::Number(md.len() as f64));
|
|
90
|
+
m.insert("mtimeMs".into(), Value::Number(ms_since_epoch(md.modified())));
|
|
91
|
+
m.insert("atimeMs".into(), Value::Number(ms_since_epoch(md.accessed())));
|
|
92
|
+
m.insert("birthtimeMs".into(), Value::Number(ms_since_epoch(md.created())));
|
|
93
|
+
#[cfg(unix)]
|
|
94
|
+
{
|
|
95
|
+
use std::os::unix::fs::MetadataExt;
|
|
96
|
+
m.insert("mode".into(), Value::Number(md.mode() as f64));
|
|
97
|
+
m.insert("uid".into(), Value::Number(md.uid() as f64));
|
|
98
|
+
m.insert("gid".into(), Value::Number(md.gid() as f64));
|
|
99
|
+
m.insert("ino".into(), Value::Number(md.ino() as f64));
|
|
100
|
+
}
|
|
101
|
+
Value::object(m)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── cores ─────────────────────────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
pub fn read_file_core(args: &[Value]) -> Result<Value, Value> {
|
|
107
|
+
std::fs::read_to_string(path_arg(args, 0))
|
|
108
|
+
.map(|s| Value::String(s.into()))
|
|
109
|
+
.map_err(io_err)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
pub fn read_file_bytes_core(args: &[Value]) -> Result<Value, Value> {
|
|
113
|
+
std::fs::read(path_arg(args, 0))
|
|
114
|
+
.map(|b| Value::Array(VmRef::new(b.into_iter().map(|x| Value::Number(x as f64)).collect())))
|
|
115
|
+
.map_err(io_err)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// Write a string, or a byte array (numbers 0–255), to a file.
|
|
119
|
+
pub fn write_file_core(args: &[Value]) -> Result<Value, Value> {
|
|
120
|
+
let path = path_arg(args, 0);
|
|
121
|
+
let res = match args.get(1) {
|
|
122
|
+
Some(Value::Array(a)) => {
|
|
123
|
+
let bytes: Vec<u8> = a
|
|
124
|
+
.borrow()
|
|
125
|
+
.iter()
|
|
126
|
+
.map(|v| if let Value::Number(n) = v { *n as u8 } else { 0 })
|
|
127
|
+
.collect();
|
|
128
|
+
std::fs::write(&path, bytes)
|
|
129
|
+
}
|
|
130
|
+
Some(v) => std::fs::write(&path, v.to_display_string()),
|
|
131
|
+
None => std::fs::write(&path, ""),
|
|
132
|
+
};
|
|
133
|
+
res.map(|_| Value::Null).map_err(io_err)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
pub fn append_file_core(args: &[Value]) -> Result<Value, Value> {
|
|
137
|
+
use std::io::Write;
|
|
138
|
+
let path = path_arg(args, 0);
|
|
139
|
+
let data = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
|
|
140
|
+
std::fs::OpenOptions::new()
|
|
141
|
+
.create(true)
|
|
142
|
+
.append(true)
|
|
143
|
+
.open(&path)
|
|
144
|
+
.and_then(|mut f| f.write_all(data.as_bytes()))
|
|
145
|
+
.map(|_| Value::Null)
|
|
146
|
+
.map_err(io_err)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
pub fn stat_core(args: &[Value]) -> Result<Value, Value> {
|
|
150
|
+
std::fs::metadata(path_arg(args, 0)).map(|m| stats_object(&m)).map_err(io_err)
|
|
151
|
+
}
|
|
152
|
+
pub fn lstat_core(args: &[Value]) -> Result<Value, Value> {
|
|
153
|
+
std::fs::symlink_metadata(path_arg(args, 0)).map(|m| stats_object(&m)).map_err(io_err)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
pub fn readdir_core(args: &[Value]) -> Result<Value, Value> {
|
|
157
|
+
std::fs::read_dir(path_arg(args, 0))
|
|
158
|
+
.map(|entries| {
|
|
159
|
+
let names: Vec<Value> = entries
|
|
160
|
+
.filter_map(|e| e.ok())
|
|
161
|
+
.map(|e| Value::String(e.file_name().to_string_lossy().into()))
|
|
162
|
+
.collect();
|
|
163
|
+
Value::Array(VmRef::new(names))
|
|
164
|
+
})
|
|
165
|
+
.map_err(io_err)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/// `mkdir(path[, { recursive }])` — recursive creates parents.
|
|
169
|
+
pub fn mkdir_core(args: &[Value]) -> Result<Value, Value> {
|
|
170
|
+
let path = path_arg(args, 0);
|
|
171
|
+
let recursive = opt_bool(args.get(1), "recursive");
|
|
172
|
+
let res = if recursive {
|
|
173
|
+
std::fs::create_dir_all(&path)
|
|
174
|
+
} else {
|
|
175
|
+
std::fs::create_dir(&path)
|
|
176
|
+
};
|
|
177
|
+
res.map(|_| Value::Null).map_err(io_err)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/// `rm(path[, { recursive }])` — file, or whole tree when recursive.
|
|
181
|
+
pub fn rm_core(args: &[Value]) -> Result<Value, Value> {
|
|
182
|
+
let path = path_arg(args, 0);
|
|
183
|
+
let recursive = opt_bool(args.get(1), "recursive");
|
|
184
|
+
let md = std::fs::symlink_metadata(&path);
|
|
185
|
+
let res = match md {
|
|
186
|
+
Ok(m) if m.is_dir() => {
|
|
187
|
+
if recursive {
|
|
188
|
+
std::fs::remove_dir_all(&path)
|
|
189
|
+
} else {
|
|
190
|
+
std::fs::remove_dir(&path)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
_ => std::fs::remove_file(&path),
|
|
194
|
+
};
|
|
195
|
+
res.map(|_| Value::Null).map_err(io_err)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
pub fn rmdir_core(args: &[Value]) -> Result<Value, Value> {
|
|
199
|
+
std::fs::remove_dir(path_arg(args, 0)).map(|_| Value::Null).map_err(io_err)
|
|
200
|
+
}
|
|
201
|
+
pub fn unlink_core(args: &[Value]) -> Result<Value, Value> {
|
|
202
|
+
std::fs::remove_file(path_arg(args, 0)).map(|_| Value::Null).map_err(io_err)
|
|
203
|
+
}
|
|
204
|
+
pub fn rename_core(args: &[Value]) -> Result<Value, Value> {
|
|
205
|
+
std::fs::rename(path_arg(args, 0), path_arg(args, 1)).map(|_| Value::Null).map_err(io_err)
|
|
206
|
+
}
|
|
207
|
+
pub fn copy_file_core(args: &[Value]) -> Result<Value, Value> {
|
|
208
|
+
std::fs::copy(path_arg(args, 0), path_arg(args, 1)).map(|_| Value::Null).map_err(io_err)
|
|
209
|
+
}
|
|
210
|
+
pub fn realpath_core(args: &[Value]) -> Result<Value, Value> {
|
|
211
|
+
std::fs::canonicalize(path_arg(args, 0))
|
|
212
|
+
.map(|p| Value::String(p.to_string_lossy().into()))
|
|
213
|
+
.map_err(io_err)
|
|
214
|
+
}
|
|
215
|
+
pub fn readlink_core(args: &[Value]) -> Result<Value, Value> {
|
|
216
|
+
std::fs::read_link(path_arg(args, 0))
|
|
217
|
+
.map(|p| Value::String(p.to_string_lossy().into()))
|
|
218
|
+
.map_err(io_err)
|
|
219
|
+
}
|
|
220
|
+
pub fn truncate_core(args: &[Value]) -> Result<Value, Value> {
|
|
221
|
+
let len = match args.get(1) {
|
|
222
|
+
Some(Value::Number(n)) => *n as u64,
|
|
223
|
+
_ => 0,
|
|
224
|
+
};
|
|
225
|
+
std::fs::OpenOptions::new()
|
|
226
|
+
.write(true)
|
|
227
|
+
.open(path_arg(args, 0))
|
|
228
|
+
.and_then(|f| f.set_len(len))
|
|
229
|
+
.map(|_| Value::Null)
|
|
230
|
+
.map_err(io_err)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/// `mkdtemp(prefix)` — create a uniquely-named temp dir and return its path. Node appends 6 RANDOM
|
|
234
|
+
/// characters; a timestamp suffix is both predictable (a temp-dir security smell) and collision-prone
|
|
235
|
+
/// under rapid calls. Use a random suffix and rely on `create_dir`'s exclusive semantics, retrying on
|
|
236
|
+
/// the astronomically-rare collision.
|
|
237
|
+
pub fn mkdtemp_core(args: &[Value]) -> Result<Value, Value> {
|
|
238
|
+
let prefix = path_arg(args, 0);
|
|
239
|
+
for _ in 0..16 {
|
|
240
|
+
// 10 base-36 chars derived from a random u64 (~51 bits of entropy).
|
|
241
|
+
let mut n: u64 = rand::random();
|
|
242
|
+
let mut suffix = String::with_capacity(10);
|
|
243
|
+
for _ in 0..10 {
|
|
244
|
+
suffix.push(char::from_digit((n % 36) as u32, 36).unwrap());
|
|
245
|
+
n /= 36;
|
|
246
|
+
}
|
|
247
|
+
let path = format!("{}{}", prefix, suffix);
|
|
248
|
+
match std::fs::create_dir(&path) {
|
|
249
|
+
Ok(()) => return Ok(Value::String(path.into())),
|
|
250
|
+
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
|
|
251
|
+
Err(e) => return Err(io_err(e)),
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
Err(io_err(std::io::Error::new(
|
|
255
|
+
std::io::ErrorKind::AlreadyExists,
|
|
256
|
+
"mkdtemp: could not create a unique temporary directory",
|
|
257
|
+
)))
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/// `cp(src, dest[, { recursive }])` — copy a file, or a directory tree when recursive.
|
|
261
|
+
pub fn cp_core(args: &[Value]) -> Result<Value, Value> {
|
|
262
|
+
let src = path_arg(args, 0);
|
|
263
|
+
let dest = path_arg(args, 1);
|
|
264
|
+
let recursive = opt_bool(args.get(2), "recursive");
|
|
265
|
+
copy_recursive(std::path::Path::new(&src), std::path::Path::new(&dest), recursive)
|
|
266
|
+
.map(|_| Value::Null)
|
|
267
|
+
.map_err(io_err)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
fn copy_recursive(src: &std::path::Path, dest: &std::path::Path, recursive: bool) -> std::io::Result<()> {
|
|
271
|
+
if src.is_dir() {
|
|
272
|
+
if !recursive {
|
|
273
|
+
return Err(std::io::Error::new(
|
|
274
|
+
std::io::ErrorKind::InvalidInput,
|
|
275
|
+
"cp on a directory requires { recursive: true }",
|
|
276
|
+
));
|
|
277
|
+
}
|
|
278
|
+
std::fs::create_dir_all(dest)?;
|
|
279
|
+
for entry in std::fs::read_dir(src)? {
|
|
280
|
+
let entry = entry?;
|
|
281
|
+
copy_recursive(&entry.path(), &dest.join(entry.file_name()), true)?;
|
|
282
|
+
}
|
|
283
|
+
Ok(())
|
|
284
|
+
} else {
|
|
285
|
+
if let Some(parent) = dest.parent() {
|
|
286
|
+
std::fs::create_dir_all(parent)?;
|
|
287
|
+
}
|
|
288
|
+
std::fs::copy(src, dest).map(|_| ())
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
fn opt_bool(v: Option<&Value>, key: &str) -> bool {
|
|
293
|
+
match v {
|
|
294
|
+
Some(Value::Object(o)) => o
|
|
295
|
+
.borrow()
|
|
296
|
+
.strings
|
|
297
|
+
.get(key)
|
|
298
|
+
.map(|b| b.is_truthy())
|
|
299
|
+
.unwrap_or(false),
|
|
300
|
+
_ => false,
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ── sync + promise exports ──────────────────────────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
fs_method!(read_file, read_file_promise, read_file_core);
|
|
307
|
+
fs_method!(read_file_bytes, read_file_bytes_promise, read_file_bytes_core);
|
|
308
|
+
fs_method!(write_file, write_file_promise, write_file_core);
|
|
309
|
+
fs_method!(append_file, append_file_promise, append_file_core);
|
|
310
|
+
fs_method!(stat, stat_promise, stat_core);
|
|
311
|
+
fs_method!(lstat, lstat_promise, lstat_core);
|
|
312
|
+
fs_method!(readdir, readdir_promise, readdir_core);
|
|
313
|
+
fs_method!(mkdir, mkdir_promise, mkdir_core);
|
|
314
|
+
fs_method!(rm, rm_promise, rm_core);
|
|
315
|
+
fs_method!(rmdir, rmdir_promise, rmdir_core);
|
|
316
|
+
fs_method!(unlink, unlink_promise, unlink_core);
|
|
317
|
+
fs_method!(rename, rename_promise, rename_core);
|
|
318
|
+
fs_method!(copy_file, copy_file_promise, copy_file_core);
|
|
319
|
+
fs_method!(realpath, realpath_promise, realpath_core);
|
|
320
|
+
fs_method!(readlink, readlink_promise, readlink_core);
|
|
321
|
+
fs_method!(truncate, truncate_promise, truncate_core);
|
|
322
|
+
fs_method!(mkdtemp, mkdtemp_promise, mkdtemp_core);
|
|
323
|
+
fs_method!(cp, cp_promise, cp_core);
|
|
324
|
+
|
|
325
|
+
// `exists` / `access` never error — they answer a boolean.
|
|
326
|
+
pub fn exists(args: &[Value]) -> Value {
|
|
327
|
+
Value::Bool(std::path::Path::new(&path_arg(args, 0)).exists())
|
|
328
|
+
}
|
|
329
|
+
pub fn exists_promise(args: &[Value]) -> Value {
|
|
330
|
+
promise_resolve(&[exists(args)])
|
|
331
|
+
}
|
|
332
|
+
/// `accessSync(path)` → true if the path exists (tish-friendly); the promise form resolves
|
|
333
|
+
/// `true` / rejects with an error, matching `fs/promises.access`.
|
|
334
|
+
pub fn access(args: &[Value]) -> Value {
|
|
335
|
+
exists(args)
|
|
336
|
+
}
|
|
337
|
+
pub fn access_promise(args: &[Value]) -> Value {
|
|
338
|
+
let path = path_arg(args, 0);
|
|
339
|
+
if std::path::Path::new(&path).exists() {
|
|
340
|
+
promise_resolve(&[Value::Null])
|
|
341
|
+
} else {
|
|
342
|
+
promise_reject(&[Value::String(format!("ENOENT: no such file '{}'", path).into())])
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/// `isDir(path)` — tish convenience kept for back-compat (≈ `statSync().isDirectory()`).
|
|
347
|
+
pub fn is_dir(args: &[Value]) -> Value {
|
|
348
|
+
Value::Bool(std::path::Path::new(&path_arg(args, 0)).is_dir())
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/// `fs.constants` — the access-mode flags.
|
|
352
|
+
pub fn constants() -> Value {
|
|
353
|
+
let mut m = ObjectMap::default();
|
|
354
|
+
m.insert("F_OK".into(), Value::Number(0.0));
|
|
355
|
+
m.insert("R_OK".into(), Value::Number(4.0));
|
|
356
|
+
m.insert("W_OK".into(), Value::Number(2.0));
|
|
357
|
+
m.insert("X_OK".into(), Value::Number(1.0));
|
|
358
|
+
Value::object(m)
|
|
359
|
+
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
//! N OS threads (one per CPU, pinned via core_affinity)
|
|
11
11
|
//! ├─ single-threaded tokio runtime per thread
|
|
12
12
|
//! ├─ SO_REUSEPORT-bound TcpListener per thread
|
|
13
|
-
//! └─ hyper HTTP/1.1
|
|
13
|
+
//! └─ hyper HTTP/1.1 server (`http1::Builder`)
|
|
14
14
|
//! │
|
|
15
15
|
//! ├─ async per-connection state machine (no OS thread)
|
|
16
16
|
//! ├─ static-route fast path (lock-free ArcSwap<HashMap> from
|
|
@@ -23,13 +23,17 @@
|
|
|
23
23
|
//!
|
|
24
24
|
//! * Removes tiny_http's thread-per-connection model (the bottleneck on
|
|
25
25
|
//! every macOS/Linux Tish HTTP server at >50k concurrent connections).
|
|
26
|
-
//! * Gives HTTP/2 + TLS-ready surface for free (hyper handles the state
|
|
27
|
-
//! machine; we only have to convert our `RequestPrimitive` to/from
|
|
28
|
-
//! `http::Request` / `http::Response`).
|
|
29
26
|
//! * Shared async context with `reqwest`, `tokio-postgres`, `tokio` in
|
|
30
27
|
//! general — the tokio runtime is reused for client fetch, db, and
|
|
31
28
|
//! server accept.
|
|
32
29
|
//!
|
|
30
|
+
//! ## Protocol support
|
|
31
|
+
//!
|
|
32
|
+
//! HTTP/1.1 only today (the accept loop uses `http1::Builder`). The `hyper` dependency enables the
|
|
33
|
+
//! `http2` feature, so upgrading to an `hyper_util::server::conn::auto` builder (h2c prior-knowledge,
|
|
34
|
+
//! and HTTP/2 over TLS once ALPN is wired) is a contained follow-up — tracked separately. Do NOT
|
|
35
|
+
//! advertise HTTP/2 until that lands.
|
|
36
|
+
//!
|
|
33
37
|
//! ## Integration with the Tish VM
|
|
34
38
|
//!
|
|
35
39
|
//! The Tish handler returns a synchronous `Value`. We call it from inside
|
|
@@ -182,20 +186,37 @@ where
|
|
|
182
186
|
}
|
|
183
187
|
drop(tx);
|
|
184
188
|
|
|
185
|
-
// VM-thread dispatcher loop
|
|
189
|
+
// VM-thread dispatcher loop. Mirrors the tiny_http path (#384): `recv_timeout` rather than a
|
|
190
|
+
// blocking `recv` so a handler's scheduled timers still FIRE when the server is idle, and
|
|
191
|
+
// `drain_timers()` runs after every handler call AND on each idle tick. Handler calls all run on
|
|
192
|
+
// this single VM thread, so the timer registry lives here — without the drain a `setTimeout`
|
|
193
|
+
// scheduled from a hyper handler would never fire and never be freed.
|
|
186
194
|
let mut count = 0usize;
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
195
|
+
loop {
|
|
196
|
+
match rx.recv_timeout(std::time::Duration::from_millis(250)) {
|
|
197
|
+
Ok((req_prim, resp_tx)) => {
|
|
198
|
+
let req_value = req_prim.into_value_pub();
|
|
199
|
+
// `handler` is the generic `F: Fn(&[Value]) -> Value` — call it directly. (`.call(..)`
|
|
200
|
+
// here would bind the *unstable* `Fn::call` trait method, which takes a tuple, not
|
|
201
|
+
// `&[Value]`.)
|
|
202
|
+
let response_value = handler(&[req_value]);
|
|
203
|
+
crate::timers::drain_timers();
|
|
204
|
+
let resp_prim = ResponsePrimitive::from_value_pub(&response_value);
|
|
205
|
+
let _ = resp_tx.send(resp_prim);
|
|
206
|
+
|
|
207
|
+
count += 1;
|
|
208
|
+
if max_requests.map(|m| count >= m).unwrap_or(false) {
|
|
209
|
+
stop.store(true, Ordering::Relaxed);
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
Err(std_mpsc::RecvTimeoutError::Timeout) => {
|
|
214
|
+
crate::timers::drain_timers();
|
|
215
|
+
if stop.load(Ordering::Relaxed) {
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
Err(std_mpsc::RecvTimeoutError::Disconnected) => break,
|
|
199
220
|
}
|
|
200
221
|
}
|
|
201
222
|
|
|
@@ -295,7 +316,9 @@ async fn handle_request(
|
|
|
295
316
|
|
|
296
317
|
// Slow path: cross the mpsc to the VM thread.
|
|
297
318
|
let (method, url, path, query, headers, body_bytes) = extract_request(req).await;
|
|
298
|
-
|
|
319
|
+
// Lossy UTF-8 decode to match the tiny_http path (`http.rs`): a body with invalid UTF-8 becomes
|
|
320
|
+
// replacement chars, NOT an empty string — `req.body` is a String and both backends must agree.
|
|
321
|
+
let body = String::from_utf8_lossy(&body_bytes).into_owned();
|
|
299
322
|
let prim = RequestPrimitive::new_pub(method, url, path, query, headers, body);
|
|
300
323
|
|
|
301
324
|
let (resp_tx, resp_rx) = std_mpsc::sync_channel::<ResponsePrimitive>(1);
|