Skip to content

Commit 76a5f6a

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: nodejs#37179 Reviewed-By: Benjamin Gruenbaum <[email protected]>
1 parent e5744a3 commit 76a5f6a

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
@@ -1179,6 +1179,55 @@ The `atime` and `mtime` arguments follow these rules:
11791179
* If the value can not be converted to a number, or is `NaN`, `Infinity` or
11801180
`-Infinity`, an `Error` will be thrown.
11811181
1182+
### `fsPromises.watch(filename[, options])`
1183+
<!-- YAML
1184+
added: REPLACEME
1185+
-->
1186+
1187+
* `filename` {string|Buffer|URL}
1188+
* `options` {string|Object}
1189+
* `persistent` {boolean} Indicates whether the process should continue to run
1190+
as long as files are being watched. **Default:** `true`.
1191+
* `recursive` {boolean} Indicates whether all subdirectories should be
1192+
watched, or only the current directory. This applies when a directory is
1193+
specified, and only on supported platforms (See [caveats][]). **Default:**
1194+
`false`.
1195+
* `encoding` {string} Specifies the character encoding to be used for the
1196+
filename passed to the listener. **Default:** `'utf8'`.
1197+
* `signal` {AbortSignal} An {AbortSignal} used to signal when the watcher
1198+
should stop.
1199+
* Returns: {AsyncIterator} of objects with the properties:
1200+
* `eventType` {string} The type of change
1201+
* `filename` {string|Buffer} The name of the file changed.
1202+
1203+
Returns an async iterator that watches for changes on `filename`, where `filename`
1204+
is either a file or a directory.
1205+
1206+
```js
1207+
const { watch } = require('fs/promises');
1208+
1209+
const ac = new AbortController();
1210+
const { signal } = ac;
1211+
setTimeout(() => ac.abort(), 10000);
1212+
1213+
(async () => {
1214+
try {
1215+
const watcher = watch(__filename, { signal });
1216+
for await (const event of watcher)
1217+
console.log(event);
1218+
} catch (err) {
1219+
if (err.name === 'AbortError')
1220+
return;
1221+
throw err;
1222+
}
1223+
})();
1224+
```
1225+
1226+
On most platforms, `'rename'` is emitted whenever a filename appears or
1227+
disappears in the directory.
1228+
1229+
All the [caveats][] for `fs.watch()` also apply to `fsPromises.watch()`.
1230+
11821231
### `fsPromises.writeFile(file, data[, options])`
11831232
<!-- YAML
11841233
added: v10.0.0
@@ -3459,7 +3508,7 @@ changes:
34593508
as long as files are being watched. **Default:** `true`.
34603509
* `recursive` {boolean} Indicates whether all subdirectories should be
34613510
watched, or only the current directory. This applies when a directory is
3462-
specified, and only on supported platforms (See [Caveats][]). **Default:**
3511+
specified, and only on supported platforms (See [caveats][]). **Default:**
34633512
`false`.
34643513
* `encoding` {string} Specifies the character encoding to be used for the
34653514
filename passed to the listener. **Default:** `'utf8'`.
@@ -6526,7 +6575,6 @@ A call to `fs.ftruncate()` or `filehandle.truncate()` can be used to reset
65266575
the file contents.
65276576
65286577
[#25741]: https://github.com/nodejs/node/issues/25741
6529-
[Caveats]: #fs_caveats
65306578
[Common System Errors]: errors.md#errors_common_system_errors
65316579
[File access constants]: #fs_file_access_constants
65326580
[MDN-Date]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date
@@ -6536,6 +6584,7 @@ the file contents.
65366584
[Naming Files, Paths, and Namespaces]: https://docs.microsoft.com/en-us/windows/desktop/FileIO/naming-a-file
65376585
[Readable Stream]: stream.md#stream_class_stream_readable
65386586
[Writable Stream]: stream.md#stream_class_stream_writable
6587+
[caveats]: #fs_caveats
65396588
[`AHAFS`]: https://www.ibm.com/developerworks/aix/library/au-aix_event_infrastructure/
65406589
[`Buffer.byteLength`]: buffer.md#buffer_static_method_buffer_bytelength_string_encoding
65416590
[`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)