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 <[email protected]> PR-URL: #37179 Reviewed-By: Benjamin Gruenbaum <[email protected]>
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‎

Lines changed: 51 additions & 2 deletions
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‎

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

7879
constkHandle=Symbol('kHandle');
7980
constkFd=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‎

Lines changed: 114 additions & 5 deletions
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-
consterrors=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
constassert=require('internal/assert');
2954

3055
constkOldStatus=Symbol('kOldStatus');
@@ -91,7 +116,7 @@ StatWatcher.prototype[kFSStatWatcherStart] = function(filename,
91116
validateUint32(interval,'interval');
92117
consterr=this._handle.start(toNamespacedPath(filename),interval);
93118
if(err){
94-
consterror=errors.uvException({
119+
consterror=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-
consterror=errors.uvException({
204+
consterror=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-
consterror=errors.uvException({
244+
consterror=uvException({
220245
errno: err,
221246
syscall: 'watch',
222247
path: filename,
@@ -270,10 +295,94 @@ ObjectDefineProperty(FSEvent.prototype, 'owner',{
270295
set(v){returnthis[owner_symbol]=v;}
271296
});
272297

298+
asyncfunction*watch(filename,options={}){
299+
constpath=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+
constreason='is invalid encoding';
315+
thrownewERR_INVALID_ARG_VALUE(encoding,'encoding',reason);
316+
}
317+
318+
if(signal?.aborted)
319+
thrownewAbortError();
320+
321+
consthandle=newFSEvent();
322+
letres;
323+
letrej;
324+
constoncancel=()=>{
325+
handle.close();
326+
rej(newAbortError());
327+
};
328+
329+
try{
330+
signal?.addEventListener('abort',oncancel,{once: true});
331+
332+
letpromise=newPromise((resolve,reject)=>{
333+
res=resolve;
334+
rej=reject;
335+
});
336+
337+
handle.onchange=(status,eventType,filename)=>{
338+
if(status<0){
339+
consterror=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+
consterr=handle.start(path,persistent,recursive,encoding);
354+
if(err){
355+
consterror=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+
throwerror;
365+
}
366+
367+
while(!signal?.aborted){
368+
yieldawaitpromise;
369+
promise=newPromise((resolve,reject)=>{
370+
res=resolve;
371+
rej=reject;
372+
});
373+
}
374+
thrownewAbortError();
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‎

Lines changed: 3 additions & 0 deletions
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)