A viable approach which does not utilize a proxy was to use a generic implementation of an around method-modifier. The latter can be seen as a specialized case of function-wrapping.
Such a modifier accepts two functions, proceed and handler as well as a target-object as its 3 parameters. It does return a function which again is going to return the result of the handler function. The latter does get invoked within the context of the (optionally) provided target while also getting passed the proceed-function, its own handler-reference and the modified function's arguments-array.
Thus, based on such a modifier, the OP could achieve the expected behavior by modifying e.g. an array instance's sort-method like this ...
const arr = [2, 1];
// reassign the array instance's specifically modified `sort`.
arr.sort = around(arr.sort, notifyAboutFinishedTask, arr);
... where notifyAboutFinishedTask is the handler function which implements exactly what the OP is looking for ...
"... only one notification after an array sort"
... Example code ...
// implementation of `sort`-specific `around`-handler.
function notifyAboutFinishedTask(proceed, handler, args) {
const arr = this;
// original task.
proceed.apply(arr, args);
// additional notification.
console.log('\narray sorted');
console.log('arr ...', arr);
console.log('arguments ...', args);
}
const arr = [2, 1];
console.log('unsorted arr ...', arr);
// reassign the array instance's specifically modified `sort`.
arr.sort = around(arr.sort, notifyAboutFinishedTask, arr);
// invoking sort twice, once with and once without sort function.
arr.sort();
arr.sort((a, b) => b - a);
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
// implementation of an `around` modifier.
function around(proceed, handler, target) {
return function (...args) {
return handler.call(target ?? null, proceed, handler, args);
};
}
</script>