Skip to content

Commit 5fecb22

Browse files
MoLowbenjamingr
authored andcommitted
fs, stream: initial Symbol.dispose and Symbol.asyncDispose support
Co-authored-by: Benjamin Gruenbaum <benjamingr@gmail.com> PR-URL: nodejs#48518 Reviewed-By: Robert Nagy <ronagy@icloud.com> Reviewed-By: Erick Wendel <erick.workspace@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 404f3ab commit 5fecb22

File tree

9 files changed

+93
-0
lines changed

9 files changed

+93
-0
lines changed

doc/api/fs.md

+10
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,16 @@ On Linux, positional writes don't work when the file is opened in append mode.
813813
The kernel ignores the position argument and always appends the data to
814814
the end of the file.
815815
816+
#### `filehandle[Symbol.asyncDispose]()`
817+
818+
<!-- YAML
819+
added: REPLACEME
820+
-->
821+
822+
> Stability: 1 - Experimental
823+
824+
An alias for `filehandle.close()`.
825+
816826
### `fsPromises.access(path[, mode])`
817827
818828
<!-- YAML

doc/api/stream.md

+11
Original file line numberDiff line numberDiff line change
@@ -1902,6 +1902,17 @@ option. In the code example above, data will be in a single chunk if the file
19021902
has less then 64 KiB of data because no `highWaterMark` option is provided to
19031903
[`fs.createReadStream()`][].
19041904

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

19071918
<!-- YAML

lib/internal/fs/promises.js

+5
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;
@@ -241,6 +242,10 @@ class FileHandle extends EventEmitterMixin(JSTransferable) {
241242
return this[kClosePromise];
242243
};
243244

245+
async [SymbolAsyncDispose]() {
246+
return this.close();
247+
}
248+
244249
/**
245250
* @typedef {import('../webstreams/readablestream').ReadableStream
246251
* } ReadableStream

lib/internal/per_context/primordials.js

+6
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

+13
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ const {
88
SafeMap,
99
SafeWeakMap,
1010
StringPrototypeStartsWith,
11+
Symbol,
12+
SymbolDispose,
13+
SymbolAsyncDispose,
1114
globalThis,
1215
} = primordials;
1316

@@ -72,6 +75,8 @@ function prepareExecution(options) {
7275
initializeDeprecations();
7376
require('internal/dns/utils').initializeDns();
7477

78+
setupSymbolDisposePolyfill();
79+
7580
if (isMainThread) {
7681
assert(internalBinding('worker').isMainThread);
7782
// Worker threads will get the manifest in the message handler.
@@ -109,6 +114,14 @@ function prepareExecution(options) {
109114
}
110115
}
111116

117+
function setupSymbolDisposePolyfill() {
118+
// TODO(MoLow): Remove this polyfill once Symbol.dispose and Symbol.asyncDispose are available in V8.
119+
// eslint-disable-next-line node-core/prefer-primordials
120+
Symbol.dispose ??= SymbolDispose;
121+
// eslint-disable-next-line node-core/prefer-primordials
122+
Symbol.asyncDispose ??= SymbolAsyncDispose;
123+
}
124+
112125
function setupUserModules() {
113126
initializeCJSLoader();
114127
initializeESMLoader();

lib/internal/streams/readable.js

+11
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;
@@ -66,6 +67,7 @@ const {
6667
ERR_STREAM_PUSH_AFTER_EOF,
6768
ERR_STREAM_UNSHIFT_AFTER_END_EVENT,
6869
},
70+
AbortError,
6971
} = require('internal/errors');
7072
const { validateObject } = require('internal/validators');
7173

@@ -226,6 +228,15 @@ Readable.prototype[EE.captureRejectionSymbol] = function(err) {
226228
this.destroy(err);
227229
};
228230

231+
Readable.prototype[SymbolAsyncDispose] = function() {
232+
let error;
233+
if (!this.destroyed) {
234+
error = this.readableEnded ? null : new AbortError();
235+
this.destroy(error);
236+
}
237+
return new Promise((resolve, reject) => eos(this, (err) => (err && err !== error ? reject(err) : resolve(null))));
238+
};
239+
229240
// Manually shove something into the read() buffer.
230241
// This returns true if the highWaterMark has not been hit yet,
231242
// similar to how Writable.write() returns true if you should
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());
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

+2
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)