Skip to content

Commit e50c316

Browse files
MoLowbenjamingr
authored andcommitted
fs, stream: initial Symbol.dispose and Symbol.asyncDispose support
Co-authored-by: Benjamin Gruenbaum <[email protected]> PR-URL: #48518 Reviewed-By: Robert Nagy <[email protected]> Reviewed-By: Erick Wendel <[email protected]> Reviewed-By: Yagiz Nizipli <[email protected]> Reviewed-By: Matteo Collina <[email protected]>
1 parent 0f912a7 commit e50c316

File tree

9 files changed

+93
-0
lines changed

9 files changed

+93
-0
lines changed

doc/api/fs.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,16 @@ On Linux, positional writes don't work when the file is opened in append mode.
817817
The kernel ignores the position argument and always appends the data to
818818
the end of the file.
819819
820+
#### `filehandle[Symbol.asyncDispose]()`
821+
822+
<!-- YAML
823+
added: REPLACEME
824+
-->
825+
826+
> Stability: 1 - Experimental
827+
828+
An alias for `filehandle.close()`.
829+
820830
### `fsPromises.access(path[, mode])`
821831
822832
<!-- YAML

doc/api/stream.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1904,6 +1904,17 @@ option. In the code example above, data will be in a single chunk if the file
19041904
has less then 64 KiB of data because no `highWaterMark` option is provided to
19051905
[`fs.createReadStream()`][].
19061906

1907+
##### `readable[Symbol.asyncDispose]()`
1908+
1909+
<!-- YAML
1910+
added: REPLACEME
1911+
-->
1912+
1913+
> Stability: 1 - Experimental
1914+
1915+
Calls [`readable.destroy()`][readable-destroy] with an `AbortError` and returns
1916+
a promise that fulfills when the stream is finished.
1917+
19071918
##### `readable.compose(stream[, options])`
19081919

19091920
<!-- YAML

lib/internal/fs/promises.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const {
1414
SafeArrayIterator,
1515
SafePromisePrototypeFinally,
1616
Symbol,
17+
SymbolAsyncDispose,
1718
Uint8Array,
1819
FunctionPrototypeBind,
1920
} = primordials;
@@ -246,6 +247,10 @@ class FileHandle extends EventEmitterMixin(JSTransferable) {
246247
return this[kClosePromise];
247248
};
248249

250+
async [SymbolAsyncDispose]() {
251+
return this.close();
252+
}
253+
249254
/**
250255
* @typedef {import('../webstreams/readablestream').ReadableStream
251256
* } ReadableStream

lib/internal/per_context/primordials.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,12 @@ function copyPrototype(src, dest, prefix) {
228228
copyPrototype(original.prototype, primordials, `${name}Prototype`);
229229
});
230230

231+
// Define Symbol.Dispose and Symbol.AsyncDispose
232+
// Until these are defined by the environment.
233+
// TODO(MoLow): Remove this polyfill once Symbol.dispose and Symbol.asyncDispose are available in V8.
234+
primordials.SymbolDispose ??= primordials.SymbolFor('nodejs.dispose');
235+
primordials.SymbolAsyncDispose ??= primordials.SymbolFor('nodejs.asyncDispose');
236+
231237
// Create copies of intrinsic objects that require a valid `this` to call
232238
// static methods.
233239
// Refs: https://www.ecma-international.org/ecma-262/#sec-promise.all

lib/internal/process/pre_execution.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ const {
88
ObjectGetOwnPropertyDescriptor,
99
SafeMap,
1010
StringPrototypeStartsWith,
11+
Symbol,
12+
SymbolDispose,
13+
SymbolAsyncDispose,
1114
globalThis,
1215
} = primordials;
1316

@@ -82,6 +85,8 @@ function prepareExecution(options) {
8285

8386
require('internal/dns/utils').initializeDns();
8487

88+
setupSymbolDisposePolyfill();
89+
8590
if (isMainThread) {
8691
assert(internalBinding('worker').isMainThread);
8792
// Worker threads will get the manifest in the message handler.
@@ -119,6 +124,14 @@ function prepareExecution(options) {
119124
}
120125
}
121126

127+
function setupSymbolDisposePolyfill() {
128+
// TODO(MoLow): Remove this polyfill once Symbol.dispose and Symbol.asyncDispose are available in V8.
129+
// eslint-disable-next-line node-core/prefer-primordials
130+
Symbol.dispose ??= SymbolDispose;
131+
// eslint-disable-next-line node-core/prefer-primordials
132+
Symbol.asyncDispose ??= SymbolAsyncDispose;
133+
}
134+
122135
function setupUserModules(isLoaderWorker = false) {
123136
initializeCJSLoader();
124137
initializeESMLoader(isLoaderWorker);

lib/internal/streams/readable.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const {
3131
ObjectSetPrototypeOf,
3232
Promise,
3333
SafeSet,
34+
SymbolAsyncDispose,
3435
SymbolAsyncIterator,
3536
Symbol,
3637
} = primordials;
@@ -67,6 +68,7 @@ const {
6768
ERR_STREAM_UNSHIFT_AFTER_END_EVENT,
6869
ERR_UNKNOWN_ENCODING,
6970
},
71+
AbortError,
7072
} = require('internal/errors');
7173
const { validateObject } = require('internal/validators');
7274

@@ -234,6 +236,15 @@ Readable.prototype[EE.captureRejectionSymbol] = function(err) {
234236
this.destroy(err);
235237
};
236238

239+
Readable.prototype[SymbolAsyncDispose] = function() {
240+
let error;
241+
if (!this.destroyed) {
242+
error = this.readableEnded ? null : new AbortError();
243+
this.destroy(error);
244+
}
245+
return new Promise((resolve, reject) => eos(this, (err) => (err && err !== error ? reject(err) : resolve(null))));
246+
};
247+
237248
// Manually shove something into the read() buffer.
238249
// This returns true if the highWaterMark has not been hit yet,
239250
// similar to how Writable.write() returns true if you should
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const { promises: fs } = require('fs');
5+
6+
async function doOpen() {
7+
const fh = await fs.open(__filename);
8+
fh.on('close', common.mustCall());
9+
await fh[Symbol.asyncDispose]();
10+
}
11+
12+
doOpen().then(common.mustCall());
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const { Readable } = require('stream');
5+
const assert = require('assert');
6+
7+
{
8+
const read = new Readable({
9+
read() {}
10+
});
11+
read.resume();
12+
13+
read.on('end', common.mustNotCall('no end event'));
14+
read.on('close', common.mustCall());
15+
read.on('error', common.mustCall((err) => {
16+
assert.strictEqual(err.name, 'AbortError');
17+
}));
18+
19+
read[Symbol.asyncDispose]().then(common.mustCall(() => {
20+
assert.strictEqual(read.errored.name, 'AbortError');
21+
assert.strictEqual(read.destroyed, true);
22+
}));
23+
}

typings/primordials.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,8 @@ declare namespace primordials {
434434
export const SymbolFor: typeof Symbol.for
435435
export const SymbolKeyFor: typeof Symbol.keyFor
436436
export const SymbolAsyncIterator: typeof Symbol.asyncIterator
437+
export const SymbolDispose: typeof Symbol // TODO(MoLow): use typeof Symbol.dispose when it's available
438+
export const SymbolAsyncDispose: typeof Symbol // TODO(MoLow): use typeof Symbol.asyncDispose when it's available
437439
export const SymbolHasInstance: typeof Symbol.hasInstance
438440
export const SymbolIsConcatSpreadable: typeof Symbol.isConcatSpreadable
439441
export const SymbolIterator: typeof Symbol.iterator

0 commit comments

Comments
 (0)