Skip to content

Conversation

@CoderSerio
Copy link
Owner

@CoderSerio CoderSerio commented Dec 8, 2025

Description

This PR implements high-performance rm and rmSync APIs, aiming to be a drop-in replacement for Node.js fs.rm.

The key highlight is the introduction of Parallel Deletion powered by Rust's rayon. Unlike Node.js, which deletes files sequentially, this implementation can process directory entries in parallel when the concurrency option is enabled. This results in significant performance gains, especially for large, nested directory structures (e.g., node_modules).

Changes

  • Core Implementation:

    • Implemented rm (async task) and rmSync using Rust's std::fs.
    • Aligned behavior with Node.js fs.rm, supporting force (ignore non-existent) and recursive options.
    • Implemented standard error handling to match Node.js error codes (ENOENT, ENOTEMPTY, EISDIR).
  • Parallelism Support:

    • Introduced a concurrency option in RmOptions.
    • Integrated rayon to perform parallel recursive deletion.
    • When concurrency > 1, the directory entries are collected and processed in parallel threads, maximizing IOPS utilization on modern SSDs.
  • API Compatibility:

    • path argument handling (supports string/Buffer behavior via NAPI).
    • Compatible options structure (recursive, force, concurrency, etc.).

Benchmarks

Benchmarks demonstrate that the Rust multi-threaded implementation significantly outperforms Node.js native fs.rm, particularly in flat directory structures where syscall latency can be hidden by parallelism.

Environment:
clk: ~3.98 GHz
cpu: Apple M3
runtime: node 24.0.2 (arm64-darwin)

image

Note: Serial performance is comparable to Node.js (C++ libuv), but parallel execution unlocks the hardware's full potential.

Checklist

  • Code compiles and passes linter (cargo check, npm run lint)
  • Added tests covering recursive deletion, force option, and concurrency
  • Verified performance improvements with benchmarks
  • Updated type definitions to include concurrency in RmOptions

Summary by CodeRabbit

  • New Features

    • Added file/directory removal APIs (sync + async) with force, recursive, retry and concurrency controls.
    • Readdir enhanced with recursive scanning, withFileTypes option and richer directory entry results.
  • Documentation

    • README updated with new rm and readdir argument/return details and concurrency options.
  • Tests

    • Comprehensive test suite covering rm/rmSync behaviors and edge cases.
  • Benchmarks

    • New and revised benchmark scripts for readdir and rm; improved benchmarking workflow.
  • Chores

    • Updated build/dev dependencies for benchmarking and parallelism.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 8, 2025

Walkthrough

This PR introduces a new rm (remove files/directories) feature with async and sync APIs, configurable options including concurrency-based parallel deletion via rayon, and comprehensive test coverage. It also extends ReaddirOptions with recursive and withFileTypes fields, updates benchmarking infrastructure to use mitata, and documents the new functionality.

Changes

Cohort / File(s) Summary
Rust rm module
src/rm.rs
New module implementing async rm() and sync rmSync() functions with RmOptions (force, maxRetries, recursive, retryDelay, concurrency) for file/directory removal; includes recursive deletion with optional parallelization via rayon and comprehensive error handling.
Rust library structure
src/lib.rs
Added pub mod rm and pub use rm::* to expose the new rm module alongside existing readdir.
Rust readdir updates
src/readdir.rs
Extended ReaddirOptions struct with recursive and with_file_types fields; introduced public Dirent struct; updated error message text; exposed readdir_sync as public API.
TypeScript definitions
index.d.ts
Added rm() async, rmSync() sync function declarations and new RmOptions interface; extended ReaddirOptions with recursive and withFileTypes fields.
JavaScript exports
index.js
Added module exports for rm and rmSync mapping to native bindings.
Dependencies
Cargo.toml, package.json
Added rayon = "1.11.0" and remove_dir_all = "1.0.0" to Cargo.toml; added mitata ^1.0.34 to devDependencies in package.json.
Test suite
__test__/rm.spec.ts
New comprehensive test file covering sync/async removal of files, directories, recursive deletion, error conditions (non-empty directories, non-existent targets), force flag behavior, and deeply nested directory removal with concurrency.
Benchmarks
benchmark/rm.ts, benchmark/readdir.ts, benchmark/bench.ts
New rm benchmark comparing Node.js fs.rmSync vs hyper-fs implementations; migrated readdir benchmarks from tinybench to mitata with new benchmark groups (names only, withFileTypes, recursive); added command-line filtering to bench.ts.
Documentation
README.md
Added concurrency option to readdir; documented rm function with arguments (path, options) and return type; documented readdir return type and rm options (force, maxRetries, recursive, retryDelay, concurrency).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • src/rm.rs: Recursive deletion logic with rayon-based parallelization, error mapping, and async/sync dual APIs merit careful review
  • test/rm.spec.ts: Verify test coverage adequately exercises force flag, recursive behavior, concurrency, and error conditions
  • Cargo.toml dependencies: Confirm rayon and remove_dir_all are appropriate and their versions stable
  • benchmark/readdir.ts: Verify mitata migration produces correct benchmark grouping and comparison output

Possibly related PRs

Poem

🐰 A swift removal comes today,
With threads that make the rm-ing play,
No force required, but options sweet,
Concurrent cleanup, fast and neat!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.08% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'Feat/carbon dev' is vague and does not clearly convey the main change; it appears to reference a branch name rather than describing the feature being implemented. Consider updating the title to something more descriptive like 'Add async rm/rmSync APIs with concurrency support' to clearly communicate the primary change to reviewers scanning the history.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/carbon-dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai
Copy link

coderabbitai bot commented Dec 8, 2025

Note

Docstrings generation - SUCCESS
Generated docstrings for this pull request at #3

coderabbitai bot added a commit that referenced this pull request Dec 8, 2025
Docstrings generation was requested by @CoderSerio.

* #2 (comment)

The following files were modified:

* `benchmark/bench.ts`
* `benchmark/rm.ts`
* `src/readdir.rs`
* `src/rm.rs`
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (12)
package.json (1)

73-74: Mitata devDependency aligns with the new benchmarking setup

Adding mitata as a devDependency fits the benchmark tooling changes; no issues here. If you’ve fully migrated away from tinybench, you can optionally drop it later to keep devDeps lean.

src/lib.rs (1)

14-18: rm module export wiring is consistent and straightforward

Re-exporting rm via pub mod rm; and pub use rm::*; mirrors the existing readdir pattern and cleanly exposes the new API. If the crate grows more modules, consider tightening re-exports (e.g., explicit symbols) to avoid future name collisions, but it’s fine as-is.

benchmark/bench.ts (1)

8-24: Benchmark file filtering is solid; message could be slightly friendlier

The case-insensitive .ts filtering and early exit behavior look good. Minor nit: when no CLI filter is provided and no benchmarks are found, the log will say filter "undefined"—if you care about UX, you could special-case the “no filter” scenario for a cleaner message, but it’s not blocking.

README.md (1)

41-86: API docs match the new options; consider reflecting Buffer path support

The added concurrency flag for readdir and the expanded rm options/return type nicely document the new behavior and differences from Node’s core APIs. Since the implementation reportedly accepts string/Buffer paths via N-API, you might optionally update the path signature here (e.g., string | Buffer) so the README matches the actual supported types.

benchmark/rm.ts (1)

1-97: Benchmark design is solid and highlights concurrency benefits

The benchmark setup (warmup runs, per-implementation fresh directories, and averaged hrtime measurements) is well thought out and should give stable comparative numbers between Node and hyper-fs, including the concurrency: 4 case. One minor consideration: a depth of 100 for the nested structure may run into path-length quirks on some Windows environments; if you plan to run these benches there, you might reduce depth and compensate with more branching instead.

__test__/rm.spec.ts (1)

1-217: rm/rmSync test coverage is excellent; only minor polish possible

This suite does a great job exercising both sync/async paths across the main behaviors (files, empty/non-empty dirs, recursive flag, force handling, and concurrency on nested trees), which should make regressions very obvious. As minor polish, you could (a) assert on error.code in addition to message regex when mimicking Node error codes, and (b) optionally add cleanup for the temp directories if you anticipate running the suite very frequently—but neither is required for correctness.

index.d.ts (2)

11-29: Readdir JSDoc does not match the actual TypeScript API surface

The JSDoc here still describes the Node-style callback API and encoding option, but the exported readdir/readdirSync functions are Promise/return-value based and the ReaddirOptions interface adds skipHidden and concurrency (and omits encoding). This can confuse consumers reading the d.ts.

Consider updating the JSDoc to:

  • Describe the Promise-returning overload (Promise<string[] | Dirent[]>) instead of a callback.
  • Document skipHidden and concurrency and remove/clarify unsupported fields like encoding.

36-58: Tighten rm typing and align rm JSDoc with implemented options

Two minor API-surface issues here:

  1. rm is declared as Promise<unknown> even though the Rust task resolves to (). Typing this as Promise<void> would better reflect the actual behavior and improve TS ergonomics.
  2. RmOptions includes concurrency, but the JSDoc comment only lists force, maxRetries, recursive, and retryDelay, and still refers to a Node-style callback signature.

If you want the d.ts to be self-explanatory, consider:

  • Changing rm to Promise<void>.
  • Updating the JSDoc to remove the callback parameter and include concurrency (and clearly note that maxRetries/retryDelay are currently accepted but not implemented, if that’s the case).
src/readdir.rs (2)

8-22: Rust-side JSDoc is still describing the old callback-style Node API

This comment block documents fs.readdir(path, options, callback) with an encoding option, but the exposed N-API bindings (readdir/readdirSync) are Promise/return-value based and support additional options (skip_hidden, concurrency) via ReaddirOptions.

If you’re using this as the canonical reference for the JS binding, it’d be clearer to update it to:

  • Describe the Promise-returning API shape.
  • Reflect recursive, withFileTypes, and any non-standard flags like skipHidden/concurrency.

48-52: Minor note on ENOENT message wording

The ENOENT string uses "readdir '{}'" whereas Node’s own error messages typically say "scandir '{}'" for fs.readdir. If you want to be byte-for-byte compatible with Node (e.g., for snapshot tests), consider mirroring Node’s wording; otherwise this is fine as-is.

src/rm.rs (2)

33-72: Concurrency flag only toggles parallelism and max_retries / retry_delay are currently unused

A few behavioral details worth making explicit:

  • In remove_recursive, concurrency is only used as a boolean toggle:

    let concurrency = opts.concurrency.unwrap_or(0);
    if concurrency > 1 {
        // parallel via par_iter
    } else {
        // serial loop
    }

    The numeric value doesn’t control the rayon thread pool size; it just decides whether to use par_iter at all. In src/readdir.rs, concurrency does directly map to the rayon pool size via Parallelism::RayonNewPool(n), so there’s a subtle inconsistency across APIs.

  • max_retries and retry_delay are accepted in RmOptions but never applied in the removal logic (e.g., on transient EBUSY/EPERM/EMFILE-like failures).

None of this is strictly wrong, but it may surprise users expecting:

  • A consistent meaning for concurrency across readdir and rm.
  • Node-like retry behavior when specifying maxRetries / retryDelay.

At minimum, consider:

  • Documenting that (for now) concurrency > 1 just enables parallel traversal using the global rayon pool, without controlling its size.
  • Either wiring max_retries / retry_delay into a simple retry loop for known transient errors, or documenting that they’re currently ignored.

56-67: Error mapping for non-recursive directory removal could rely on ErrorKind::DirectoryNotEmpty instead of string-matching

In the non-recursive branch you special-case ENOTEMPTY like this:

fs::remove_dir(path).map_err(|e| {
  if e.kind() == std::io::ErrorKind::AlreadyExists || e.to_string().contains("not empty") {
    Error::from_reason(format!(
      "ENOTEMPTY: directory not empty, rm '{}'",
      path.to_string_lossy()
    ))
  } else {
    Error::from_reason(e.to_string())
  }
})?;

Relying on e.to_string().contains("not empty") is fragile (locale-dependent and OS-string-format dependent). Using ErrorKind::DirectoryNotEmpty would be more robust and self-documenting:

- if e.kind() == std::io::ErrorKind::AlreadyExists || e.to_string().contains("not empty") {
+ if matches!(e.kind(), std::io::ErrorKind::DirectoryNotEmpty | std::io::ErrorKind::AlreadyExists) {

This keeps the intended ENOTEMPTY mapping but avoids depending on the textual message.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a4f5e6c and c8bf7bc.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (12)
  • Cargo.toml (1 hunks)
  • README.md (2 hunks)
  • __test__/rm.spec.ts (1 hunks)
  • benchmark/bench.ts (1 hunks)
  • benchmark/readdir.ts (1 hunks)
  • benchmark/rm.ts (1 hunks)
  • index.d.ts (2 hunks)
  • index.js (1 hunks)
  • package.json (1 hunks)
  • src/lib.rs (1 hunks)
  • src/readdir.rs (2 hunks)
  • src/rm.rs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/lib.rs (2)
src/rm.rs (1)
  • rm (123-125)
src/readdir.rs (1)
  • readdir (185-187)
__test__/rm.spec.ts (1)
src/rm.rs (1)
  • rm (123-125)
index.d.ts (1)
src/rm.rs (1)
  • rm (123-125)
src/rm.rs (2)
index.d.ts (1)
  • RmOptions (50-56)
src/readdir.rs (2)
  • compute (176-178)
  • resolve (179-181)
🔇 Additional comments (3)
Cargo.toml (1)

12-18: Rayon/remove_dir_all additions look appropriate for parallel rm

Using rayon for configurable concurrency and remove_dir_all for recursive deletion fits the rm implementation goals; nothing blocking from this manifest change alone. Just ensure your MSRV/target platforms are compatible with these crate versions as part of your normal release checks.

index.js (1)

574-578: rm/rmSync exports correctly mirror native binding

Hooking module.exports.rm / rmSync directly to nativeBinding.rm / rmSync is consistent with the existing readdir exports and keeps the JS surface thin. No issues here.

benchmark/readdir.ts (1)

1-42: Benchmark structure and mitata usage look solid

The switch to mitata with grouped benchmarks, node_modulescwd fallback, and the extra diagnostic logging are all reasonable. The Node.js vs Hyper-FS comparisons use matching options (withFileTypes, recursive) and the final await run({ colors: true }) is idiomatic for mitata.

No functional issues from the review perspective.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c8bf7bc and ae72083.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (1)
  • src/rm.rs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/rm.rs (2)
index.d.ts (1)
  • RmOptions (50-56)
src/readdir.rs (2)
  • compute (176-178)
  • resolve (179-181)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: stable - x86_64-pc-windows-msvc - node@22
🔇 Additional comments (3)
src/rm.rs (3)

75-99: Previous critical issue with empty-string paths has been resolved.

The past review flagged that empty strings were being mapped to ".", which could recursively delete the CWD. The current implementation at line 76 uses Path::new(&path_str) directly without any special-case mapping. When path_str is empty, the subsequent path.exists() check (or better, symlink_metadata() after the above fix) will return false/error, triggering either silent success with force: true or an ENOENT error, which is safe and appropriate behavior.


101-129: LGTM! Async task implementation follows napi-rs patterns correctly.

The RmTask struct and Task trait implementation properly delegate to the core remove function, and the public rm and rm_sync exports correctly expose async and sync variants to JavaScript.


40-54: The parallel deletion implementation is correct; no issues with error handling semantics. Both the parallel path using try_for_each and the sequential path return immediately on the first error, resulting in identical behavior. Rayon's try_for_each synchronously waits for spawned work before propagating errors, and the parent directory removal is only attempted if all child deletions succeed—matching standard recursive deletion semantics.

Likely an incorrect or invalid review comment.

Comment on lines +23 to +31
#[napi(object)]
#[derive(Clone)]
pub struct RmOptions {
pub force: Option<bool>,
pub max_retries: Option<u32>,
pub recursive: Option<bool>,
pub retry_delay: Option<u32>,
pub concurrency: Option<u32>,
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Implement retry logic or remove unused max_retries and retry_delay fields.

The RmOptions struct declares max_retries and retry_delay fields that are never used in the implementation. Users can pass these options, but they will be silently ignored, creating an API surface inconsistency with the promised functionality.

Either implement the retry logic (wrapping operations in a retry loop with exponential backoff based on these options) or remove these fields from RmOptions to avoid misleading users.

Implementation guidance

If implementing retry logic, wrap the operations in remove_recursive with a retry loop that:

  • Catches transient filesystem errors (e.g., PermissionDenied, ResourceBusy)
  • Retries up to max_retries times with retry_delay milliseconds between attempts
  • Only retries on errors that are likely transient, not permanent failures like ENOENT

Comment on lines +58 to +67
fs::remove_dir(path).map_err(|e| {
if e.kind() == std::io::ErrorKind::AlreadyExists || e.to_string().contains("not empty") {
Error::from_reason(format!(
"ENOTEMPTY: directory not empty, rm '{}'",
path.to_string_lossy()
))
} else {
Error::from_reason(e.to_string())
}
})?;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix incorrect error kind check for directory-not-empty errors.

Line 59 checks for std::io::ErrorKind::AlreadyExists, which is for "file already exists" errors, not "directory not empty" errors. Rust provides std::io::ErrorKind::DirectoryNotEmpty (stable since 1.17.0) for this purpose. The fallback string check .contains("not empty") is fragile and platform-dependent.

Apply this diff to use the correct error kind:

     } else {
       fs::remove_dir(path).map_err(|e| {
-        if e.kind() == std::io::ErrorKind::AlreadyExists || e.to_string().contains("not empty") {
+        if e.kind() == std::io::ErrorKind::DirectoryNotEmpty {
           Error::from_reason(format!(
             "ENOTEMPTY: directory not empty, rm '{}'",
             path.to_string_lossy()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fs::remove_dir(path).map_err(|e| {
if e.kind() == std::io::ErrorKind::AlreadyExists || e.to_string().contains("not empty") {
Error::from_reason(format!(
"ENOTEMPTY: directory not empty, rm '{}'",
path.to_string_lossy()
))
} else {
Error::from_reason(e.to_string())
}
})?;
fs::remove_dir(path).map_err(|e| {
if e.kind() == std::io::ErrorKind::DirectoryNotEmpty {
Error::from_reason(format!(
"ENOTEMPTY: directory not empty, rm '{}'",
path.to_string_lossy()
))
} else {
Error::from_reason(e.to_string())
}
})?;
🤖 Prompt for AI Agents
In src/rm.rs around lines 58 to 67, the error mapping currently checks for
std::io::ErrorKind::AlreadyExists and falls back to matching the error string
for "not empty" which is incorrect and fragile; change the condition to check
for std::io::ErrorKind::DirectoryNotEmpty (stable since 1.17) and remove the
string-based `.contains("not empty")` fallback so the code maps
directory-not-empty errors using the proper ErrorKind and otherwise returns the
original error string.

Comment on lines +87 to +96
if !path.exists() {
if force {
// If force is true, silently succeed when path doesn't exist
return Ok(());
}
return Err(Error::from_reason(format!(
"ENOENT: no such file or directory, rm '{}'",
path.to_string_lossy()
)));
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use try_exists() or symlink_metadata() to properly handle broken symlinks.

Line 87 uses path.exists(), which follows symlinks and returns false for broken symlinks (symlinks pointing to non-existent targets). This means a broken symlink would be treated as non-existent, requiring force: true to delete or returning ENOENT otherwise, even though the symlink itself exists and should be removable.

Apply this diff to handle broken symlinks correctly:

 fn remove(path_str: String, options: Option<RmOptions>) -> Result<()> {
   let path = Path::new(&path_str);
 
   let opts = options.unwrap_or(RmOptions {
     force: Some(false),
     recursive: Some(false),
     max_retries: None,
     retry_delay: None,
     concurrency: None,
   });
   let force = opts.force.unwrap_or(false);
 
-  if !path.exists() {
+  // Use symlink_metadata to detect broken symlinks (which exist but point to non-existent targets)
+  if fs::symlink_metadata(path).is_err() {
     if force {
       // If force is true, silently succeed when path doesn't exist
       return Ok(());

This ensures that broken symlinks are treated as existing paths and can be deleted without requiring force: true.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if !path.exists() {
if force {
// If force is true, silently succeed when path doesn't exist
return Ok(());
}
return Err(Error::from_reason(format!(
"ENOENT: no such file or directory, rm '{}'",
path.to_string_lossy()
)));
}
// Use symlink_metadata to detect broken symlinks (which exist but point to non-existent targets)
if fs::symlink_metadata(path).is_err() {
if force {
// If force is true, silently succeed when path doesn't exist
return Ok(());
}
return Err(Error::from_reason(format!(
"ENOENT: no such file or directory, rm '{}'",
path.to_string_lossy()
)));
}
🤖 Prompt for AI Agents
In src/rm.rs around lines 87 to 96, the code uses path.exists() which follows
symlinks and treats broken symlinks as non-existent; change the check to detect
the presence of the path itself (including broken symlinks) by using
path.symlink_metadata().is_ok() (or Path::try_exists() if preferred) so broken
symlinks are considered existing and can be removed without requiring force;
update the conditional to use that check and keep the existing force/ENOENT
logic intact.

@CoderSerio CoderSerio merged commit cf97abe into main Dec 8, 2025
15 checks passed
@CoderSerio
Copy link
Owner Author

The potential issues reported above are tracked and scheduled for resolution before the 1.0 release.

@coderabbitai coderabbitai bot mentioned this pull request Dec 9, 2025
10 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants