diff --git a/doc/api/process.md b/doc/api/process.md index 8b210a1e7c06..62e91a195c5f 100644 --- a/doc/api/process.md +++ b/doc/api/process.md @@ -2062,6 +2062,10 @@ Since it's not possible to build Node.js without libuv, this value is always `tr > Stability: 1.1 - Active Development @@ -2071,6 +2075,7 @@ added: v22.5.0 is finalized. * `ref` {Object | Function} The reference to the resource that is being tracked. * `event` {string} The event that triggered the finalization. Defaults to 'exit'. +* Returns: {Function} A function that removes this registration when called. This function registers a callback to be called when the process emits the `exit` event if the `ref` object was not garbage collected. If the object `ref` was garbage collected @@ -2167,10 +2172,25 @@ but if it is not, `dispose` will be called when `process.exit` is called. Be careful and avoid relying on this feature for the disposal of critical resources, as it is not guaranteed that the callback will be called under all circumstances. +The returned function removes only this registration, leaving other +registrations for the same `ref` in place. Calling it more than once has no +effect. It does not hold a strong reference to `ref`. + +```js +const unregister = finalization.register(myDisposableObject, onFinalize); + +// Later, once the resource has been released manually: +unregister(); +``` + ## `process.finalization.registerBeforeExit(ref, callback)` > Stability: 1.1 - Active Development @@ -2181,6 +2201,7 @@ added: v22.5.0 is finalized. * `ref` {Object | Function} The reference to the resource that is being tracked. * `event` {string} The event that triggered the finalization. Defaults to 'beforeExit'. +* Returns: {Function} A function that removes this registration when called. This function behaves exactly like the `register`, except that the callback will be called when the process emits the `beforeExit` event if `ref` object was not garbage collected. diff --git a/lib/internal/process/finalization.js b/lib/internal/process/finalization.js index 5efc2c8d78f9..4827298bc096 100644 --- a/lib/internal/process/finalization.js +++ b/lib/internal/process/finalization.js @@ -85,9 +85,19 @@ function createFinalization() { ref.fn = fn; registry ||= new SafeFinalizationRegistry(clear); - registry.register(obj, ref); + registry.register(obj, ref, ref); refs[event].add(ref); + + // The returned function must not capture `obj`, otherwise it would + // never be garbage collected while the caller holds the function. + return function unregister() { + if (!refs[event].delete(ref)) { + return; + } + registry.unregister(ref); + uninstall(event); + }; } /** @@ -95,12 +105,13 @@ function createFinalization() { * and clean things up when the object is gc. * @param {any} obj * @param {Function} fn + * @returns {Function} A function that removes this registration. */ function register(obj, fn) { emitExperimentalWarning('process.finalization.register'); validateObject(obj, 'obj', kValidateObjectAllowFunction); - _register('exit', obj, fn); + return _register('exit', obj, fn); } /** @@ -108,12 +119,13 @@ function createFinalization() { * and clean things up when the object is gc. * @param {any} obj * @param {Function} fn + * @returns {Function} A function that removes this registration. */ function registerBeforeExit(obj, fn) { emitExperimentalWarning('process.finalization.registerBeforeExit'); validateObject(obj, 'obj', kValidateObjectAllowFunction); - _register('beforeExit', obj, fn); + return _register('beforeExit', obj, fn); } /** @@ -125,16 +137,17 @@ function createFinalization() { if (!registry) { return; } - registry.unregister(obj); for (const event of ['exit', 'beforeExit']) { for (const ref of refs[event]) { const _obj = ref.deref(); if (!_obj || _obj === obj) { refs[event].delete(ref); + registry.unregister(ref); } } - uninstall(event); } + uninstall('exit'); + uninstall('beforeExit'); } return { diff --git a/test/fixtures/process/unregister-function.mjs b/test/fixtures/process/unregister-function.mjs new file mode 100644 index 000000000000..b74a5b8203a0 --- /dev/null +++ b/test/fixtures/process/unregister-function.mjs @@ -0,0 +1,29 @@ +import { strictEqual } from 'assert' + +const calls = [] + +function onExit(obj, event) { + calls.push(`${obj.name}:${event}`) +} + +const a = { name: 'a' } +const b = { name: 'b' } + +const unregisterA = process.finalization.register(a, onExit) +const unregisterABeforeExit = process.finalization.registerBeforeExit(a, onExit) +process.finalization.register(b, onExit) +const unregisterBAgain = process.finalization.register(b, onExit) + +strictEqual(typeof unregisterA, 'function') +strictEqual(typeof unregisterABeforeExit, 'function') + +unregisterA() +unregisterA() // twice, this should not throw +unregisterABeforeExit() + +// Removing one registration keeps the others for the same object. +unregisterBAgain() + +process.on('exit', function () { + strictEqual(calls.join(','), 'b:exit') +}) diff --git a/test/parallel/test-process-finalization.mjs b/test/parallel/test-process-finalization.mjs index dddd0b8fae19..9daa1f178f92 100644 --- a/test/parallel/test-process-finalization.mjs +++ b/test/parallel/test-process-finalization.mjs @@ -12,6 +12,7 @@ const files = [ 'finalization-cleanup.mjs', 'gc-not-close.mjs', 'unregister.mjs', + 'unregister-function.mjs', 'different-registry-per-thread.mjs', ];