Skip to content

Commit fe12cc0

Browse files
jasnelltargos
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 <[email protected]> PR-URL: #37179 Reviewed-By: Benjamin Gruenbaum <[email protected]>
1 parent f5b2fe1 commit fe12cc0

File tree

5 files changed

+286
-7
lines changed

5 files changed

+286
-7
lines changed

doc/api/fs.md

+51-2
Original file line numberDiff line numberDiff line change
@@ -1181,6 +1181,55 @@ The `atime` and `mtime` arguments follow these rules:
11811181
* If the value can not be converted to a number, or is `NaN`, `Infinity` or
11821182
`-Infinity`, an `Error` will be thrown.
11831183
1184+
### `fsPromises.watch(filename[, options])`
1185+
<!-- YAML
1186+
added: REPLACEME
1187+
-->
1188+
1189+
* `filename` {string|Buffer|URL}
1190+
* `options` {string|Object}
1191+
* `persistent` {boolean} Indicates whether the process should continue to run
1192+
as long as files are being watched. **Default:** `true`.
1193+
* `recursive` {boolean} Indicates whether all subdirectories should be
1194+
watched, or only the current directory. This applies when a directory is
1195+
specified, and only on supported platforms (See [caveats][]). **Default:**
1196+
`false`.
1197+
* `encoding` {string} Specifies the character encoding to be used for the
1198+
filename passed to the listener. **Default:** `'utf8'`.
1199+
* `signal` {AbortSignal} An {AbortSignal} used to signal when the watcher
1200+
should stop.
1201+
* Returns: {AsyncIterator} of objects with the properties:
1202+
* `eventType` {string} The type of change
1203+
* `filename` {string|Buffer} The name of the file changed.
1204+
1205+
Returns an async iterator that watches for changes on `filename`, where `filename`
1206+
is either a file or a directory.
1207+
1208+
```js
1209+
const { watch } = require('fs/promises');
1210+
1211+
const ac = new AbortController();
1212+
const { signal } = ac;
1213+
setTimeout(() => ac.abort(), 10000);
1214+
1215+
(async () => {
1216+
try {
1217+
const watcher = watch(__filename, { signal });
1218+
for await (const event of watcher)
1219+
console.log(event);
1220+
} catch (err) {
1221+
if (err.name === 'AbortError')
1222+
return;
1223+
throw err;
1224+
}
1225+
})();
1226+
```
1227+
1228+
On most platforms, `'rename'` is emitted whenever a filename appears or
1229+
disappears in the directory.
1230+
1231+
All the [caveats][] for `fs.watch()` also apply to `fsPromises.watch()`.
1232+
11841233
### `fsPromises.writeFile(file, data[, options])`
11851234
<!-- YAML
11861235
added: v10.0.0
@@ -3461,7 +3510,7 @@ changes:
34613510
as long as files are being watched. **Default:** `true`.
34623511
* `recursive` {boolean} Indicates whether all subdirectories should be
34633512
watched, or only the current directory. This applies when a directory is
3464-
specified, and only on supported platforms (See [Caveats][]). **Default:**
3513+
specified, and only on supported platforms (See [caveats][]). **Default:**
34653514
`false`.
34663515
* `encoding` {string} Specifies the character encoding to be used for the
34673516
filename passed to the listener. **Default:** `'utf8'`.
@@ -6534,7 +6583,6 @@ A call to `fs.ftruncate()` or `filehandle.truncate()` can be used to reset
65346583
the file contents.
65356584
65366585
[#25741]: https://github.com/nodejs/node/issues/25741
6537-
[Caveats]: #fs_caveats
65386586
[Common System Errors]: errors.md#errors_common_system_errors
65396587
[File access constants]: #fs_file_access_constants
65406588
[MDN-Date]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date
@@ -6544,6 +6592,7 @@ the file contents.
65446592
[Naming Files, Paths, and Namespaces]: https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file
65456593
[Readable Stream]: stream.md#stream_class_stream_readable
65466594
[Writable Stream]: stream.md#stream_class_stream_writable
6595+
[caveats]: #fs_caveats
65476596
[`AHAFS`]: https://www.ibm.com/developerworks/aix/library/au-aix_event_infrastructure/
65486597
[`Buffer.byteLength`]: buffer.md#buffer_static_method_buffer_bytelength_string_encoding
65496598
[`FSEvents`]: https://developer.apple.com/documentation/coreservices/file_system_events

lib/internal/fs/promises.js

+2
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ const {
7272
} = require('internal/validators');
7373
const pathModule = require('path');
7474
const { promisify } = require('internal/util');
75+
const { watch } = require('internal/fs/watchers');
7576

7677
const kHandle = Symbol('kHandle');
7778
const kFd = Symbol('kFd');
@@ -713,6 +714,7 @@ module.exports = {
713714
writeFile,
714715
appendFile,
715716
readFile,
717+
watch,
716718
},
717719

718720
FileHandle

lib/internal/fs/watchers.js

+114-5
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,52 @@
33
const {
44
ObjectDefineProperty,
55
ObjectSetPrototypeOf,
6+
Promise,
67
Symbol,
78
} = primordials;
89

9-
const errors = require('internal/errors');
10+
const {
11+
AbortError,
12+
uvException,
13+
codes: {
14+
ERR_INVALID_ARG_VALUE,
15+
},
16+
} = require('internal/errors');
17+
1018
const {
1119
kFsStatsFieldsNumber,
1220
StatWatcher: _StatWatcher
1321
} = internalBinding('fs');
22+
1423
const { FSEvent } = internalBinding('fs_event_wrap');
1524
const { UV_ENOSPC } = internalBinding('uv');
1625
const { EventEmitter } = require('events');
26+
1727
const {
1828
getStatsFromBinding,
1929
getValidatedPath
2030
} = require('internal/fs/utils');
31+
2132
const {
2233
defaultTriggerAsyncIdScope,
2334
symbols: { owner_symbol }
2435
} = require('internal/async_hooks');
36+
2537
const { toNamespacedPath } = require('path');
26-
const { validateUint32 } = require('internal/validators');
38+
39+
const {
40+
validateAbortSignal,
41+
validateBoolean,
42+
validateObject,
43+
validateUint32,
44+
} = require('internal/validators');
45+
46+
const {
47+
Buffer: {
48+
isEncoding,
49+
},
50+
} = require('buffer');
51+
2752
const assert = require('internal/assert');
2853

2954
const kOldStatus = Symbol('kOldStatus');
@@ -90,7 +115,7 @@ StatWatcher.prototype[kFSStatWatcherStart] = function(filename,
90115
validateUint32(interval, 'interval');
91116
const err = this._handle.start(toNamespacedPath(filename), interval);
92117
if (err) {
93-
const error = errors.uvException({
118+
const error = uvException({
94119
errno: err,
95120
syscall: 'watch',
96121
path: filename
@@ -175,7 +200,7 @@ function FSWatcher() {
175200
this._handle.close();
176201
this._handle = null; // Make the handle garbage collectable.
177202
}
178-
const error = errors.uvException({
203+
const error = uvException({
179204
errno: status,
180205
syscall: 'watch',
181206
path: filename
@@ -215,7 +240,7 @@ FSWatcher.prototype[kFSWatchStart] = function(filename,
215240
recursive,
216241
encoding);
217242
if (err) {
218-
const error = errors.uvException({
243+
const error = uvException({
219244
errno: err,
220245
syscall: 'watch',
221246
path: filename,
@@ -269,10 +294,94 @@ ObjectDefineProperty(FSEvent.prototype, 'owner', {
269294
set(v) { return this[owner_symbol] = v; }
270295
});
271296

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

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
'NativeModule async_hooks',
3537
'NativeModule buffer',
3638
'NativeModule events',
@@ -49,6 +51,7 @@ const expectedModules = new Set([
4951
'NativeModule internal/fs/utils',
5052
'NativeModule internal/fs/promises',
5153
'NativeModule internal/fs/rimraf',
54+
'NativeModule internal/fs/watchers',
5255
'NativeModule internal/idna',
5356
'NativeModule internal/linkedlist',
5457
'NativeModule internal/modules/run_main',

0 commit comments

Comments
 (0)