Library / SDK
Axosoft/nsfw avatar
Axosoft/nsfw

nsfw watches whole directory trees in native code and hands Node one batched callback

A super fast and scaleable file watcher that provides a consistent interface on Linux, OSX and Windows

932 stars119 forksC++MIT

At a glance

What is it?
NSFW is a native Node module that wraps inotify, FSEventsService and ReadDirectoryChangesW behind a single promise based API, so a recursive watcher costs almost nothing on the JavaScript side. It suits projects that watch large trees, and it costs you a working Node-Gyp toolchain.
Who is it for?
Adopt nsfw when you need recursive watching over a deep tree on Linux, macOS or Windows and you can keep a native build toolchain alive in CI, because the event batching and the managed watch tree are the reason to use it. Do not adopt it for a small single directory where Node's built in fs.watch already suffices, or for a pure JavaScript build that refuses native modules.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 123 days ago.
What is it written in?
Mainly C++, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One JavaScript API over inotify, FSEventsService and ReadDirectoryChangesW

NSFW is published as the package nsfw and is described in its own documentation as a simple file watcher library for node. Its stated purpose is to be a native abstraction for Linux, Windows and OSX file watching services that keeps a consistent interface and feature set across operating systems. The mechanism differs by platform. On Linux the library recursively builds an inotify watch tree natively and collects events concurrently to the JavaScript thread. On OSX it uses the FSEventsService, which recursively watches for file system changes in a specified directory. On Windows it implements a server around the ReadDirectoryChangesW method. The practical consequence is that recursion is not something your code has to implement. You pass a single directory and the native layer handles deep trees, described in the README as recursive file watching into deep file systems at no additional cost to the Javascript layer. The package manifest confirms the shape of the build with a dependency on node-addon-api and a published file list that includes src, includes and binding.gyp.

Batched callbacks instead of one call per file event

The difference that matters most in practice is how events reach your handler. When NSFW has events and is not being throttled, it groups them in the order that they occurred and reports them to the Javascript layer in a single callback. The documentation draws the comparison directly: Node's FS.watch uses a callback for every file event that is triggered, and every callback FS.watch makes to the event queue is a big bonus to NSFW's performance when watching large file system operations, because NSFW makes only one callback with many events inside a specified throttle period. Your handler receives an array, so a rebuild that touches a thousand files arrives as one array rather than a thousand separate invocations. This is the architectural reason the module exists, and it is why the throttle window is exposed as a first class option rather than left as an internal detail. If your handler does expensive per event work, the batching changes your cost model from per file to per batch.

The native build is the first thing that can fail

Installation is where projects like this one give you the least help, and NSFW is explicit about it. The installation notes state that NSFW is a native node module and requires Node-Gyp to be functional before you can install it, and that you must have completed installing all of the dependencies listed for Node-Gyp on your operating system. No install command is offered, so the path is the one Node-Gyp itself documents rather than a shortcut invented here. The manifest reinforces the point: the package is compiled from C++ sources with CMakeLists.txt and binding.gyp at the top level, and it declares a Node engine floor of >=10.16.0. A single runtime dependency, node-addon-api, is listed. The trade-off is worth stating plainly: you gain native watch trees and batched events, and you pay for them with a compiler toolchain on every machine and every CI runner that installs the project.

The first real watcher: start, receive, stop

The usage example shows the calling convention clearly. You call the module with a directory and a callback, receive a promise for a watcher object, and then start it. The watcher exposes start and stop, so the lifetime of the watch is explicit rather than implicit in a process exit handler. This is the shape to copy first:

js
var nsfw = require('nsfw');

var watcher1;
return nsfw(
  'dir1',
  function(events) {
    // handle events
  })
  .then(function(watcher) {
    watcher1 = watcher;
    return watcher.start();
  })

Once the promise chain resolves, the directory is being watched and your callback receives the event array on every batch. The same example then calls watcher1.stop() to end the watch.

excludedPaths can change without a restart

Three options control what reaches your handler: debounceMS, errorCallback and excludedPaths. debounceMS delays notifications emitted by the library and defaults to 500 ms, so a burst of writes inside that window arrives together. excludedPaths takes an array of absolute paths to leave out of the watch, and the more interesting half is updateExcludedPaths, which lets you change that array without restarting the service. That matters for a build tool whose node_modules directory appears after install or a watcher whose ignore list grows over time. The documented example gives this second form with the options object attached:

js
var watcher2;
return nsfw(
  'dir2',
  function(events) {
  // handles other events
  },
  {
    debounceMS: 250,
    errorCallback(errors) {
      //handle errors
    },
    excludedPaths: ['dir2/node_modules']
  })

After the watcher is running, the same example calls watcher2.updateExcludedPaths(['dir2/node_modules', '.git']) to extend the ignore list live.

Numeric action codes, and errors that leave the watch running

The callback argument is an array of events covering a directory, its children or a file. Each event carries an action field, and those actions are exposed through the nsfw.actions enumeration, where CREATED is 0, DELETED is 1, MODIFIED is 2 and RENAMED is 3. A created or deleted event names its directory and file. A rename is richer, since it carries oldFile, newFile and newDirectory, so you can tell a move apart from a delete followed by a create. The values are numbers on the wire rather than strings, which keeps the native to JavaScript boundary cheap but means your code has to map them through nsfw.actions rather than compare against readable text:

js
nsfw.actions = {
  CREATED: 0,
  DELETED: 1,
  MODIFIED: 2,
  RENAMED: 3
};

Error handling is deliberately thin. errorCallback is called when an error happens, and the documentation says plainly that at the moment when an error happens the service does not stop, noting that this may change in the near future. There is no documented restart or re-arm path, so recovering from a watch failure means building it yourself around the same directory path. Keep what you need to recreate the watcher in scope rather than relying on the process staying up.

When Node's built in fs.watch is the better answer

The comparison the project draws against FS.watch is the honest benchmark for the whole project. FS.watch is built into Node, needs no compiler, and gives you a callback per event. If you are watching one shallow directory and your handler is cheap, that is enough, and nsfw buys you a native toolchain you do not want. NSFW is the better answer when the tree is deep, when the number of events per operation is large enough that per file callbacks dominate, or when you need recursive coverage without maintaining your own directory traversal. It is also the wrong tool if your deployment forbids native modules, since the package ships C++ sources and requires Node-Gyp to be functional before installation. Judge it on the shape of your file system activity rather than on its feature list.

Licence, packaging and the shape of the repository

The licence field on the repository says MIT, and package.json repeats the same identifier, with Axosoft, LLC listed as author. That is the permissive, short form choice, and it carries no copyleft obligation for code that links against the module. The published files are index.d.ts, js/src, src, includes and binding.gyp, so type declarations ship with the package and the TypeScript entry point is index.d.ts. The repository also carries a .eslintrc, a CHANGELOG.md, a yarn.lock and a docs directory, and the test entry points are visible in the scripts: lint runs eslint over js/src and js/spec, while test runs the lint step and then node js/scripts/test.js. The last push was on 2026-06-22 and the repository is not archived, so commits continue even though the release tags listed in the history are older.

Editorial conclusion

Adopt nsfw when you need recursive watching over a deep tree on Linux, macOS or Windows and you can keep a native build toolchain alive in CI, because the event batching and the managed watch tree are the reason to use it. Do not adopt it for a small single directory where Node's built in fs.watch already suffices, or for a pure JavaScript build that refuses native modules. Before committing, verify that the Node-Gyp dependency list for your operating system installs cleanly, that your Node runtime satisfies the >=10.16.0 engine field, and that the debounceMS default of 500 ms suits the latency your build or test loop needs.

Frequently asked questions

Does nsfw require a compiler toolchain to install?

Yes. The README states that NSFW is a native node module and requires Node-Gyp to be functional before you can install it, and that you must have completed installing all of the dependencies listed for Node-Gyp on your operating system.

How does nsfw differ from Node's fs.watch?

Node's fs.watch uses a callback for every file event that is triggered. NSFW groups the events in the order that they occurred and reports them to the Javascript layer in a single callback within a throttle period, and it manages recursive watching for you.

Can I change which directories nsfw ignores while it is running?

Yes. The excludedPaths option takes an array of absolute paths, and the updateExcludedPaths function updates that array without restarting the service.

What happens to the watcher when an error occurs?

The library calls errorCallback when an error happens, and at the moment the service does not stop. The README notes that this may change in the near future, so recovery logic should not assume the watch survives.

Which Node versions does nsfw support?

The package manifest declares an engines field of node >=10.16.0, and the v2.0.0 release note records that support for versions below 10.16.0 was dropped.

Official sources

  1. Axosoft/nsfw on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/axosoft-nsfw.svg)](https://hysenlabs.com/projects/axosoft-nsfw)

Was this useful?