Skip to content

Commit 88d3f74

Browse files
jasnelldanielleadams
authored andcommitted
fs: add fsPromises.watch()
An alternative to `fs.watch()` that returns an `AsyncIterator` ```js const { watch } = require('fs/promises'); (async () => { const ac = new AbortController(); const { signal } = ac; setTimeout(() => ac.abort(), 10000); const watcher = watch('file.txt', { signal }); for await (const { eventType, filename } of watcher) { console.log(eventType, filename); } })() ``` Signed-off-by: James M Snell <jasnell@gmail.com> PR-URL: #37179 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
1 parent b2b6411 commit 88d3f74

File tree

5 files changed

+285
-7
lines changed

5 files changed

+285
-7
lines changed

doc/api/fs.md

+51-2
Original file line numberDiff line numberDiff line change
@@ -1189,6 +1189,55 @@ The `atime` and `mtime` arguments follow these rules:
11891189
* If the value can not be converted to a number, or is `NaN`, `Infinity` or
11901190
`-Infinity`, an `Error` will be thrown.
11911191
1192+
### `fsPromises.watch(filename[, options])`
1193+
<!-- YAML
1194+
added: REPLACEME
1195+
-->
1196+
1197+
* `filename` {string|Buffer|URL}
1198+
* `options` {string|Object}
1199+
* `persistent` {boolean} Indicates whether the process should continue to run
1200+
as long as files are being watched. **Default:** `true`.
1201+
* `recursive` {boolean} Indicates whether all subdirectories should be
1202+
watched, or only the current directory. This applies when a directory is
1203+
specified, and only on supported platforms (See [caveats][]). **Default:**
1204+
`false`.
1205+
* `encoding` {string} Specifies the character encoding to be used for the
1206+
filename passed to the listener. **Default:** `'utf8'`.
1207+
* `signal` {AbortSignal} An {AbortSignal} used to signal when the watcher
1208+
should stop.
1209+
* Returns: {AsyncIterator} of objects with the properties:
1210+
* `eventType` {string} The type of change
1211+
* `filename` {string|Buffer} The name of the file changed.
1212+
1213+
Returns an async iterator that watches for changes on `filename`, where `filename`
1214+
is either a file or a directory.
1215+
1216+
```js
1217+
const { watch } = require('fs/promises');
1218+
1219+
const ac = new AbortController();
1220+
const { signal } = ac;
1221+
setTimeout(() => ac.abort(), 10000);
1222+
1223+
(async () => {
1224+
try {
1225+
const watcher = watch(__filename, { signal });
1226+
for await (const event of watcher)
1227+
console.log(event);
1228+
} catch (err) {
1229+
if (err.name === 'AbortError')
1230+
return;
1231+
throw err;
1232+
}
1233+
})();
1234+
```
1235+
1236+
On most platforms, `'rename'` is emitted whenever a filename appears or
1237+
disappears in the directory.
1238+
1239+
All the [caveats][] for `fs.watch()` also apply to `fsPromises.watch()`.
1240+
11921241
### `fsPromises.writeFile(file, data[, options])`
11931242
<!-- YAML
11941243
added: v10.0.0
@@ -3477,7 +3526,7 @@ changes:
34773526
as long as files are being watched. **Default:** `true`.
34783527
* `recursive` {boolean} Indicates whether all subdirectories should be
34793528
watched, or only the current directory. This applies when a directory is
3480-
specified, and only on supported platforms (See [Caveats][]). **Default:**
3529+
specified, and only on supported platforms (See [caveats][]). **Default:**
34813530
`false`.
34823531
* `encoding` {string} Specifies the character encoding to be used for the
34833532
filename passed to the listener. **Default:** `'utf8'`.
@@ -6550,7 +6599,6 @@ A call to `fs.ftruncate()` or `filehandle.truncate()` can be used to reset
65506599
the file contents.
65516600
65526601
[#25741]: https://github.com/nodejs/node/issues/25741
6553-
[Caveats]: #fs_caveats
65546602
[Common System Errors]: errors.md#errors_common_system_errors
65556603
[File access constants]: #fs_file_access_constants
65566604
[MDN-Date]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date
@@ -6560,6 +6608,7 @@ the file contents.
65606608
[Naming Files, Paths, and Namespaces]: https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file
65616609
[Readable Stream]: stream.md#stream_class_stream_readable
65626610
[Writable Stream]: stream.md#stream_class_stream_writable
6611+
[caveats]: #fs_caveats
65636612
[`AHAFS`]: https://www.ibm.com/developerworks/aix/library/au-aix_event_infrastructure/
65646613
[`Buffer.byteLength`]: buffer.md#buffer_static_method_buffer_bytelength_string_encoding
65656614
[`FSEvents`]: https://developer.apple.com/documentation/coreservices/file_system_events

lib/internal/fs/promises.js

+2
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ const {
7474
const pathModule = require('path');
7575
const { promisify } = require('internal/util');
7676
const { EventEmitterMixin } = require('internal/event_target');
77+
const { watch } = require('internal/fs/watchers');
7778

7879
const kHandle = Symbol('kHandle');
7980
const kFd = Symbol('kFd');
@@ -724,6 +725,7 @@ module.exports = {
724725
writeFile,
725726
appendFile,
726727
readFile,
728+
watch,
727729
},
728730

729731
FileHandle,

lib/internal/fs/watchers.js

+114-5
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,52 @@ const {
44
FunctionPrototypeCall,
55
ObjectDefineProperty,
66
ObjectSetPrototypeOf,
7+
Promise,
78
Symbol,
89
} = primordials;
910

10-
const errors = require('internal/errors');
11+
const {
12+
AbortError,
13+
uvException,
14+
codes: {
15+
ERR_INVALID_ARG_VALUE,
16+
},
17+
} = require('internal/errors');
18+
1119
const {
1220
kFsStatsFieldsNumber,
1321
StatWatcher: _StatWatcher
1422
} = internalBinding('fs');
23+
1524
const { FSEvent } = internalBinding('fs_event_wrap');
1625
const { UV_ENOSPC } = internalBinding('uv');
1726
const { EventEmitter } = require('events');
27+
1828
const {
1929
getStatsFromBinding,
2030
getValidatedPath
2131
} = require('internal/fs/utils');
32+
2233
const {
2334
defaultTriggerAsyncIdScope,
2435
symbols: { owner_symbol }
2536
} = require('internal/async_hooks');
37+
2638
const { toNamespacedPath } = require('path');
27-
const { validateUint32 } = require('internal/validators');
39+
40+
const {
41+
validateAbortSignal,
42+
validateBoolean,
43+
validateObject,
44+
validateUint32,
45+
} = require('internal/validators');
46+
47+
const {
48+
Buffer: {
49+
isEncoding,
50+
},
51+
} = require('buffer');
52+
2853
const assert = require('internal/assert');
2954

3055
const kOldStatus = Symbol('kOldStatus');
@@ -91,7 +116,7 @@ StatWatcher.prototype[kFSStatWatcherStart] = function(filename,
91116
validateUint32(interval, 'interval');
92117
const err = this._handle.start(toNamespacedPath(filename), interval);
93118
if (err) {
94-
const error = errors.uvException({
119+
const error = uvException({
95120
errno: err,
96121
syscall: 'watch',
97122
path: filename
@@ -176,7 +201,7 @@ function FSWatcher() {
176201
this._handle.close();
177202
this._handle = null; // Make the handle garbage collectable.
178203
}
179-
const error = errors.uvException({
204+
const error = uvException({
180205
errno: status,
181206
syscall: 'watch',
182207
path: filename
@@ -216,7 +241,7 @@ FSWatcher.prototype[kFSWatchStart] = function(filename,
216241
recursive,
217242
encoding);
218243
if (err) {
219-
const error = errors.uvException({
244+
const error = uvException({
220245
errno: err,
221246
syscall: 'watch',
222247
path: filename,
@@ -270,10 +295,94 @@ ObjectDefineProperty(FSEvent.prototype, 'owner', {
270295
set(v) { return this[owner_symbol] = v; }
271296
});
272297

298+
async function* watch(filename, options = {}) {
299+
const path = toNamespacedPath(getValidatedPath(filename));
300+
validateObject(options, 'options');
301+
302+
const {
303+
persistent = true,
304+
recursive = false,
305+
encoding = 'utf8',
306+
signal,
307+
} = options;
308+
309+
validateBoolean(persistent, 'options.persistent');
310+
validateBoolean(recursive, 'options.recursive');
311+
validateAbortSignal(signal, 'options.signal');
312+
313+
if (encoding && !isEncoding(encoding)) {
314+
const reason = 'is invalid encoding';
315+
throw new ERR_INVALID_ARG_VALUE(encoding, 'encoding', reason);
316+
}
317+
318+
if (signal?.aborted)
319+
throw new AbortError();
320+
321+
const handle = new FSEvent();
322+
let res;
323+
let rej;
324+
const oncancel = () => {
325+
handle.close();
326+
rej(new AbortError());
327+
};
328+
329+
try {
330+
signal?.addEventListener('abort', oncancel, { once: true });
331+
332+
let promise = new Promise((resolve, reject) => {
333+
res = resolve;
334+
rej = reject;
335+
});
336+
337+
handle.onchange = (status, eventType, filename) => {
338+
if (status < 0) {
339+
const error = uvException({
340+
errno: status,
341+
syscall: 'watch',
342+
path: filename
343+
});
344+
error.filename = filename;
345+
handle.close();
346+
rej(error);
347+
return;
348+
}
349+
350+
res({ eventType, filename });
351+
};
352+
353+
const err = handle.start(path, persistent, recursive, encoding);
354+
if (err) {
355+
const error = uvException({
356+
errno: err,
357+
syscall: 'watch',
358+
path: filename,
359+
message: err === UV_ENOSPC ?
360+
'System limit for number of file watchers reached' : ''
361+
});
362+
error.filename = filename;
363+
handle.close();
364+
throw error;
365+
}
366+
367+
while (!signal?.aborted) {
368+
yield await promise;
369+
promise = new Promise((resolve, reject) => {
370+
res = resolve;
371+
rej = reject;
372+
});
373+
}
374+
throw new AbortError();
375+
} finally {
376+
handle.close();
377+
signal?.removeEventListener('abort', oncancel);
378+
}
379+
}
380+
273381
module.exports = {
274382
FSWatcher,
275383
StatWatcher,
276384
kFSWatchStart,
277385
kFSStatWatcherStart,
278386
kFSStatWatcherAddOrCleanRef,
387+
watch,
279388
};

test/parallel/test-bootstrap-modules.js

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const expectedModules = new Set([
1717
'Internal Binding credentials',
1818
'Internal Binding fs',
1919
'Internal Binding fs_dir',
20+
'Internal Binding fs_event_wrap',
2021
'Internal Binding messaging',
2122
'Internal Binding module_wrap',
2223
'Internal Binding native_module',
@@ -31,6 +32,7 @@ const expectedModules = new Set([
3132
'Internal Binding types',
3233
'Internal Binding url',
3334
'Internal Binding util',
35+
'Internal Binding uv',
3436
'Internal Binding worker',
3537
'NativeModule buffer',
3638
'NativeModule events',
@@ -51,6 +53,7 @@ const expectedModules = new Set([
5153
'NativeModule internal/fs/utils',
5254
'NativeModule internal/fs/promises',
5355
'NativeModule internal/fs/rimraf',
56+
'NativeModule internal/fs/watchers',
5457
'NativeModule internal/idna',
5558
'NativeModule internal/linkedlist',
5659
'NativeModule internal/modules/run_main',

0 commit comments

Comments
 (0)