Skip to content

Commit fd44fe8

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 4339923 commit fd44fe8

File tree

9 files changed

+99
-6
lines changed

9 files changed

+99
-6
lines changed

doc/api/fs.md

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

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

+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;
@@ -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

+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

+19-6
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,23 @@
22

33
const {
44
ArrayPrototypeForEach,
5-
NumberParseInt,
6-
ObjectDefineProperties,
7-
ObjectDefineProperty,
8-
ObjectGetOwnPropertyDescriptor,
9-
SafeMap,
10-
StringPrototypeStartsWith,
115
Date,
126
DatePrototypeGetFullYear,
137
DatePrototypeGetMonth,
148
DatePrototypeGetDate,
159
DatePrototypeGetHours,
1610
DatePrototypeGetMinutes,
1711
DatePrototypeGetSeconds,
12+
NumberParseInt,
13+
ObjectDefineProperties,
14+
ObjectDefineProperty,
15+
ObjectGetOwnPropertyDescriptor,
16+
SafeMap,
1817
String,
18+
StringPrototypeStartsWith,
19+
Symbol,
20+
SymbolDispose,
21+
SymbolAsyncDispose,
1922
globalThis,
2023
} = primordials;
2124

@@ -90,6 +93,8 @@ function prepareExecution(options) {
9093

9194
require('internal/dns/utils').initializeDns();
9295

96+
setupSymbolDisposePolyfill();
97+
9398
if (isMainThread) {
9499
assert(internalBinding('worker').isMainThread);
95100
// Worker threads will get the manifest in the message handler.
@@ -127,6 +132,14 @@ function prepareExecution(options) {
127132
}
128133
}
129134

135+
function setupSymbolDisposePolyfill() {
136+
// TODO(MoLow): Remove this polyfill once Symbol.dispose and Symbol.asyncDispose are available in V8.
137+
// eslint-disable-next-line node-core/prefer-primordials
138+
Symbol.dispose ??= SymbolDispose;
139+
// eslint-disable-next-line node-core/prefer-primordials
140+
Symbol.asyncDispose ??= SymbolAsyncDispose;
141+
}
142+
130143
function setupUserModules(isLoaderWorker = false) {
131144
initializeCJSLoader();
132145
initializeESMLoader(isLoaderWorker);

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;
@@ -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
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)