// JS-target primitives and `with_lock` impl for cross-process file
// locking. Uses node:fs's openSync with O_EXCL for the sentinel and
// wraps f() in a JS try/finally so that an `abort()` inside the
// callback still triggers the release (MoonBit's try/catch doesn't
// propagate through JS-level abort exceptions).
///| Try to acquire a lock file. Returns true on success, false if
///| already locked.
extern "js" fn _try_acquire(path : String) -> Bool =
#| (path) => {
#| const fs = require("node:fs");
#| try {
#| const fd = fs.openSync(path, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o600);
#| fs.closeSync(fd);
#| return true;
#| } catch (e) {
#| if (e.code === "EEXIST") return false;
#| throw e;
#| }
#| }
///| Return the mtime of `path` in epoch milliseconds, or -1 if the file
///| does not exist / cannot be stat'd.
extern "js" fn _stat_mtime_ms(path : String) -> Int64 =
#| (path) => {
#| const fs = require("node:fs");
#| try {
#| const st = fs.statSync(path);
#| return BigInt(Math.trunc(st.mtimeMs));
#| } catch (e) {
#| return BigInt(-1);
#| }
#| }
///| Run async `callback` inside a JS try/finally so `release_fn` fires
///| even if `callback` throws at the JS level.
extern "js" fn _run_with_finally_async(
callback : async () -> Unit,
release_fn : () -> Unit
) -> @ffi.Promise[Unit] =
#| (callback, release_fn) => {
#| return (async () => {
#| try { await callback(); }
#| finally { release_fn(); }
#| })();
#| }
///| Acquires a lock on `path` (creating it exclusively if absent),
///| runs `f` asynchronously, then releases the lock in a JS try/finally.
///| Retry loop inlined to avoid a separate async helper that the
///| MoonBit test scheduler sometimes mishandles on repeated runs.
pub async fn[T] with_lock(path : String, f : async () -> T) -> T {
let ttl_ms = _effective_stale_ttl_ms()
let mut attempts = 0
while !_try_acquire(path) {
attempts = attempts + 1
if attempts % 100 == 0 {
let mtime = _stat_mtime_ms(path)
if mtime > 0L {
let age = _now_ms_lock() - mtime
if age > ttl_ms.to_int64() {
let _ = _force_remove(path)
continue
}
}
}
let max_attempts = (ttl_ms / 10) + 100
if attempts > max_attempts {
abort("lock timeout: " + path)
}
sleep_ms(10)
}
// Typed holder survives the async () -> Unit boundary of the JS wrapper.
let holder : Array[T] = []
_run_with_finally_async(async fn() { holder.push(f()) }, fn() { _release(path) }).wait()
holder[0]
}
///| Set atime/mtime on a file. JS-only test helper.
extern "js" fn ffi_set_mtime(path : String, mtime_ms : Int64) -> Unit =
#| (path, mtimeMs) => {
#| const fs = require("node:fs");
#| const t = Number(mtimeMs) / 1000;
#| fs.utimesSync(path, t, t);
#| }
///| Set an env var. JS-only test helper (moonbitlang/core/env is
///| get-only, and we don't want an extra moonbitlang/x/sys dep just
///| for the inline stale-sentinel test).
extern "js" fn ffi_set_env_lock(key : String, value : String) -> Unit =
#| (key, value) => { globalThis.process.env[key] = value; }
///| Unset an env var. Same JS-only caveat.
extern "js" fn ffi_unset_env_lock(key : String) -> Unit =
#| (key) => { delete globalThis.process.env[key]; }