Skip to content

Commit c1f1080

Browse files
committed
util: add util.disposer helper to wrap a dispose function
Add `util.disposer` and `util.asyncDisposer` to conveniently wrap a function to be a disposable, and allow it to be used with `using` declaration.
1 parent 7ffa029 commit c1f1080

File tree

6 files changed

+314
-0
lines changed

6 files changed

+314
-0
lines changed

doc/api/util.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3655,6 +3655,89 @@ Returns `true` if the value is a built-in {WeakSet} instance.
36553655
util.types.isWeakSet(new WeakSet()); // Returns true
36563656
```
36573657
3658+
## Disposer APIs
3659+
3660+
> Stability: 1 - Experimental
3661+
3662+
### `util.asyncDisposer(onDispose)`
3663+
3664+
<!-- YAML
3665+
added: REPLACEME
3666+
-->
3667+
3668+
* `onDispose` {Function} A dispose function returning a promise
3669+
* Returns: {AsyncDisposer}
3670+
3671+
Create a convenient wrapper on the given async function that can be used
3672+
with `using` declaration.
3673+
3674+
If an error is thrown in the function, instead of returning a promise,
3675+
the error will be wrapped in a rejected promise.
3676+
3677+
```mjs
3678+
{
3679+
await using _ = util.disposer(async function disposer() {
3680+
// Performing async disposing actions...
3681+
});
3682+
3683+
// Doing some works...
3684+
3685+
} // When this scope exits, the disposer function is invoked and awaited.
3686+
```
3687+
3688+
### `util.disposer(onDispose)`
3689+
3690+
<!-- YAML
3691+
added: REPLACEME
3692+
-->
3693+
3694+
* `onDispose` {Function} A dispose function
3695+
* Returns: {Disposer}
3696+
3697+
Create a convenient wrapper on the given function that can be used with
3698+
`using` declaration.
3699+
3700+
```js
3701+
{
3702+
using _ = util.disposer(function disposer() {
3703+
// Performing disposing actions...
3704+
});
3705+
3706+
// Doing some works...
3707+
3708+
} // When this scope exits, the disposer function is invoked.
3709+
```
3710+
3711+
### Class: `util.AsyncDisposer`
3712+
3713+
<!-- YAML
3714+
added: REPLACEME
3715+
-->
3716+
3717+
A convenience wrapper on an async dispose function.
3718+
3719+
#### `asyncDisposer[Symbol.asyncDispose]()`
3720+
3721+
Invokes the function specified in `util.asyncDisposer(onDispose)`.
3722+
3723+
Multiple invocations on this method only result in a single
3724+
invocation of the `onDispose` function.
3725+
3726+
### Class: `util.Disposer`
3727+
3728+
<!-- YAML
3729+
added: REPLACEME
3730+
-->
3731+
3732+
A convenience wrapper on a dispose function.
3733+
3734+
#### `disposer[Symbol.dispose]()`
3735+
3736+
Invokes the function specified in `util.disposer(onDispose)`.
3737+
3738+
Multiple invocations on this method only result in a single
3739+
invocation of the `onDispose` function.
3740+
36583741
## Deprecated APIs
36593742
36603743
The following APIs are deprecated and should no longer be used. Existing

lib/internal/util/disposer.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
'use strict';
2+
3+
const {
4+
PromiseWithResolvers,
5+
SymbolAsyncDispose,
6+
SymbolDispose,
7+
} = primordials;
8+
const {
9+
validateFunction,
10+
} = require('internal/validators');
11+
12+
class Disposer {
13+
#disposed = false;
14+
#onDispose;
15+
constructor(onDispose) {
16+
validateFunction(onDispose, 'disposeFn');
17+
this.#onDispose = onDispose;
18+
}
19+
20+
dispose() {
21+
if (this.#disposed) {
22+
return;
23+
}
24+
this.#disposed = true;
25+
this.#onDispose();
26+
}
27+
28+
[SymbolDispose]() {
29+
this.dispose();
30+
}
31+
}
32+
33+
class AsyncDisposer {
34+
/**
35+
* @type {PromiseWithResolvers<void>}
36+
*/
37+
#disposeDeferred;
38+
#onDispose;
39+
constructor(onDispose) {
40+
validateFunction(onDispose, 'disposeFn');
41+
this.#onDispose = onDispose;
42+
}
43+
44+
dispose() {
45+
if (this.#disposeDeferred === undefined) {
46+
this.#disposeDeferred = PromiseWithResolvers();
47+
try {
48+
const ret = this.#onDispose();
49+
this.#disposeDeferred.resolve(ret);
50+
} catch (err) {
51+
this.#disposeDeferred.reject(err);
52+
}
53+
}
54+
return this.#disposeDeferred.promise;
55+
}
56+
57+
[SymbolAsyncDispose]() {
58+
return this.dispose();
59+
}
60+
}
61+
62+
function disposer(disposeFn) {
63+
return new Disposer(disposeFn);
64+
}
65+
66+
function asyncDisposer(disposeFn) {
67+
return new AsyncDisposer(disposeFn);
68+
}
69+
70+
module.exports = {
71+
disposer,
72+
asyncDisposer,
73+
};

lib/util.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,3 +491,9 @@ defineLazyProperties(
491491
'internal/util/diff',
492492
['diff'],
493493
);
494+
495+
defineLazyProperties(
496+
module.exports,
497+
'internal/util/disposer',
498+
['disposer', 'asyncDisposer', 'Disposer', 'AsyncDisposer'],
499+
);
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
'use strict';
2+
3+
// This test checks that the semantics of `util.asyncDisposer` are as described in
4+
// the API docs
5+
6+
const common = require('../common');
7+
const assert = require('node:assert');
8+
const { asyncDisposer } = require('node:util');
9+
const test = require('node:test');
10+
11+
test('util.asyncDisposer should throw on non-function first parameter', () => {
12+
const invalidDisposers = [
13+
null,
14+
undefined,
15+
42,
16+
'string',
17+
{},
18+
[],
19+
Symbol('symbol'),
20+
];
21+
for (const invalidDisposer of invalidDisposers) {
22+
assert.throws(() => {
23+
asyncDisposer(invalidDisposer);
24+
}, {
25+
code: 'ERR_INVALID_ARG_TYPE',
26+
name: 'TypeError',
27+
});
28+
}
29+
});
30+
31+
test('util.asyncDisposer should create a AsyncDisposable object', async () => {
32+
const disposeFn = common.mustCall();
33+
const disposable = asyncDisposer(disposeFn);
34+
assert.strictEqual(typeof disposable, 'object');
35+
assert.strictEqual(disposable[Symbol.dispose], undefined);
36+
assert.strictEqual(typeof disposable[Symbol.asyncDispose], 'function');
37+
38+
// Multiple calls to asyncDispose should not throw and only invoke the function once.
39+
const p1 = disposable[Symbol.asyncDispose]();
40+
const p2 = disposable[Symbol.asyncDispose]();
41+
assert.strictEqual(p1, p2);
42+
await p1;
43+
});
44+
45+
test('AsyncDisposer[Symbol.asyncDispose] must be invoked with an AsyncDisposer', () => {
46+
const disposeFn = common.mustNotCall();
47+
const disposable = asyncDisposer(disposeFn);
48+
assert.throws(() => {
49+
disposable[Symbol.asyncDispose].call({}); // Call with a non-AsyncDisposer object
50+
}, TypeError);
51+
52+
assert.throws(() => {
53+
disposable.dispose.call({}); // Call with a non-AsyncDisposer object
54+
}, TypeError);
55+
});
56+
57+
test('AsyncDisposer[Symbol.asyncDispose] should reject if the disposerFn throws sync', async () => {
58+
const disposeFn = common.mustCall(() => {
59+
throw new Error('Disposer error');
60+
});
61+
const disposable = asyncDisposer(disposeFn);
62+
const promise = disposable[Symbol.asyncDispose]();
63+
64+
await assert.rejects(promise, {
65+
name: 'Error',
66+
message: 'Disposer error',
67+
});
68+
});
69+
70+
test('Disposer[Symbol.asyncDispose] should reject if the disposerFn rejects', async () => {
71+
const disposeFn = common.mustCall(() => {
72+
return Promise.reject(new Error('Disposer error'));
73+
});
74+
const disposable = asyncDisposer(disposeFn);
75+
const promise = disposable[Symbol.asyncDispose]();
76+
77+
await assert.rejects(promise, {
78+
name: 'Error',
79+
message: 'Disposer error',
80+
});
81+
});

test/parallel/test-util-disposer.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
'use strict';
2+
3+
// This test checks that the semantics of `util.disposer` are as described in
4+
// the API docs
5+
6+
const common = require('../common');
7+
const assert = require('node:assert');
8+
const { disposer } = require('node:util');
9+
const test = require('node:test');
10+
11+
test('util.disposer should throw on non-function first parameter', () => {
12+
const invalidDisposers = [
13+
null,
14+
undefined,
15+
42,
16+
'string',
17+
{},
18+
[],
19+
Symbol('symbol'),
20+
];
21+
for (const invalidDisposer of invalidDisposers) {
22+
assert.throws(() => {
23+
disposer(invalidDisposer);
24+
}, {
25+
code: 'ERR_INVALID_ARG_TYPE',
26+
name: 'TypeError',
27+
});
28+
}
29+
});
30+
31+
test('util.disposer should create a Disposable object', () => {
32+
const disposeFn = common.mustCall();
33+
const disposable = disposer(disposeFn);
34+
assert.strictEqual(typeof disposable, 'object');
35+
assert.strictEqual(typeof disposable[Symbol.dispose], 'function');
36+
assert.strictEqual(disposable[Symbol.asyncDispose], undefined);
37+
disposable[Symbol.dispose]();
38+
// Multiple calls to dispose should not throw and only invoke the function once.
39+
disposable[Symbol.dispose]();
40+
});
41+
42+
test('Disposer[Symbol.dispose] must be invoked with an Disposer', () => {
43+
const disposeFn = common.mustNotCall();
44+
const disposable = disposer(disposeFn);
45+
assert.throws(() => {
46+
disposable[Symbol.dispose].call({}); // Call with a non-Disposer object
47+
}, TypeError);
48+
49+
assert.throws(() => {
50+
disposable.dispose.call({}); // Call with a non-Disposer object
51+
}, TypeError);
52+
});
53+
54+
test('Disposer[Symbol.dispose] should throw if the disposerFn throws', () => {
55+
const disposeFn = common.mustCall(() => {
56+
throw new Error('Disposer error');
57+
});
58+
const disposable = disposer(disposeFn);
59+
assert.throws(() => {
60+
disposable[Symbol.dispose]();
61+
}, {
62+
name: 'Error',
63+
message: 'Disposer error',
64+
});
65+
66+
// Multiple calls to dispose should not throw and only invoke the function once.
67+
disposable[Symbol.dispose]();
68+
});

tools/doc/type-parser.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ const customTypesMap = {
6767
'AsyncHook': 'async_hooks.html#async_hookscreatehookcallbacks',
6868
'AsyncResource': 'async_hooks.html#class-asyncresource',
6969

70+
'AsyncDisposer': 'util.html#class-utilasyncdisposer',
71+
'Disposer': 'util.html#class-utildisposer',
72+
7073
'brotli options': 'zlib.html#class-brotlioptions',
7174

7275
'Buffer': 'buffer.html#class-buffer',

0 commit comments

Comments
 (0)