Repository: vercel/next.js
File: crates/napi/src/lockfile.rs (Rust)

Code before:
) -> napi::Result<Option<External<JsLockfile>>> {
    tokio::task::spawn_blocking(move || lockfile_try_acquire_sync(path, content))
        .await
        .context("panicked while attempting to acquire lockfile")?
}

/// Read the content of a lockfile without acquiring it.
/// Returns None if the file doesn't exist or can't be read.
#[napi]
pub fn lockfile_read_sync(path: String) -> Option<String> {
    let mut file = File::open(&path).ok()?;
    let mut content = String::new();
    file.read_to_string(&mut content).ok()?;
    Some(content)
}

/// Async version of lockfile_read_sync.
#[napi]
pub async fn lockfile_read(path: String) -> napi::Result<Option<String>> {
    Ok(
        tokio::task::spawn_blocking(move || lockfile_read_sync(path))
            .await
            .context("panicked while attempting to read lockfile")?,
    )
}

#[napi]
pub fn lockfile_unlock_sync(
    #[napi(ts_arg_type = "{ __napiType: \"Lockfile\" }")] lockfile: External<JsLockfile>,
) {
    // We don't need the file handle anymore, so we don't need to call `File::unlock`. Locks are
    // released during `drop`. Remove it from the `ManuallyDrop` wrapper.
    let Some(inner): Option<LockfileInner> = lockfile
        .lock()
        .expect("poisoned: another thread panicked during `lockfile_unlock_sync`?")
        .take()
    else {
        return;
    };

    // - We use `FILE_FLAG_DELETE_ON_CLOSE` on Windows, so we don't need to delete the file there.
    // - Ignore possible errors while removing the file, it only matters that we release the lock.
    // - Delete *before* releasing the lock to avoid race conditions where we might accidentally
    //   delete another process's lockfile. This relies on POSIX semantics, letting us delete an
    //   open file.
    #[cfg(not(windows))]
    let _ = std::fs::remove_file(inner.path);

    drop(inner.file);
}


Review comment (suggestion):
The read here isn't actually doing anything special, you could just read this with node's filesystem APIs, but if you think this makes the code cleaner, I'm happy to have this API.

Code after:
    };
}

#[napi(ts_return_type = "Promise<{ __napiType: \"Lockfile\" } | null>")]
pub async fn lockfile_try_acquire(
    path: String,
    content: Option<String>,
) -> napi::Result<Option<External<JsLockfile>>> {
    tokio::task::spawn_blocking(move || lockfile_try_acquire_sync(path, content))
        .await
        .context("panicked while attempting to acquire lockfile")?
}

#[napi]
pub fn lockfile_unlock_sync(
    #[napi(ts_arg_type = "{ __napiType: \"Lockfile\" }")] lockfile: External<JsLockfile>,
) {
    // We don't need the file handle anymore, so we don't need to call `File::unlock`. Locks are
    // released during `drop`. Remove it from the `ManuallyDrop` wrapper.
    let Some(inner): Option<LockfileInner> = lockfile
        .lock()
        .expect("poisoned: another thread panicked during `lockfile_unlock_sync`?")
        .take()
    else {
        return;
    };

    // - We use `FILE_FLAG_DELETE_ON_CLOSE` on Windows, so we don't need to delete the file there.
    // - Ignore possible errors while removing the file, it only matters that we release the lock.
    // - Delete *before* releasing the lock to avoid race conditions where we might accidentally
    //   delete another process's lockfile. This relies on POSIX semantics, letting us delete an
    //   open file.
    #[cfg(not(windows))]
    let _ = std::fs::remove_file(inner.path);

    drop(inner.file);
}

#[napi]
pub async fn lockfile_unlock(
    #[napi(ts_arg_type = "{ __napiType: \"Lockfile\" }")] lockfile: External<JsLockfile>,
) -> napi::Result<()> {
    Ok(
        tokio::task::spawn_blocking(move || lockfile_unlock_sync(lockfile))
            .await
            .context("panicked while attempting to unlock lockfile")?,
    )
}