node/lib/diagnostics_channel.js
Stephen Belanger 5551dfc299
diagnostics_channel: return original thenable
This makes tracePromise return the original thenable to allow custom
thenable types to retain their methods rather than producing the
chained result type.

Signed-off-by: Stephen Belanger <admin@stephenbelanger.com>
PR-URL: https://github.com/nodejs/node/pull/62407
Reviewed-By: René <contact.9a5d6388@renegade334.me.uk>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gerhard Stöbich <deb2001-github@yahoo.de>
2026-06-26 10:02:49 +00:00

654 lines
16 KiB
JavaScript

'use strict';
const {
ArrayPrototypeAt,
ArrayPrototypeIndexOf,
ArrayPrototypePush,
ArrayPrototypePushApply,
ArrayPrototypeSlice,
ArrayPrototypeSplice,
ObjectDefineProperty,
ObjectGetPrototypeOf,
ObjectSetPrototypeOf,
PromisePrototype,
PromisePrototypeThen,
ReflectApply,
SafeFinalizationRegistry,
SafeMap,
SymbolDispose,
SymbolHasInstance,
} = primordials;
const {
codes: {
ERR_INVALID_ARG_TYPE,
},
} = require('internal/errors');
const {
validateFunction,
} = require('internal/validators');
const { triggerUncaughtException } = internalBinding('errors');
const dc_binding = internalBinding('diagnostics_channel');
const { subscribers: subscriberCounts } = dc_binding;
const { WeakReference, kEmptyObject } = require('internal/util');
const { isPromise } = require('internal/util/types');
// Can't delete when weakref count reaches 0 as it could increment again.
// Only GC can be used as a valid time to clean up the channels map.
class WeakRefMap extends SafeMap {
#finalizers = new SafeFinalizationRegistry((key) => {
// Check that the key doesn't have any value before deleting, as the WeakRef for the key
// may have been replaced since finalization callbacks aren't synchronous with GC.
if (!this.has(key)) this.delete(key);
});
set(key, value) {
this.#finalizers.register(value, key);
return super.set(key, new WeakReference(value));
}
get(key) {
return super.get(key)?.get();
}
has(key) {
return !!this.get(key);
}
incRef(key) {
return super.get(key)?.incRef();
}
decRef(key) {
return super.get(key)?.decRef();
}
}
function markActive(channel) {
// eslint-disable-next-line no-use-before-define
ObjectSetPrototypeOf(channel, ActiveChannel.prototype);
channel._subscribers = [];
channel._stores = new SafeMap();
}
function maybeMarkInactive(channel) {
// When there are no more active subscribers or bound, restore to fast prototype.
if (!channel._subscribers.length && !channel._stores.size) {
// eslint-disable-next-line no-use-before-define
ObjectSetPrototypeOf(channel, Channel.prototype);
channel._subscribers = undefined;
channel._stores = undefined;
}
}
class RunStoresScope {
#stack;
constructor(activeChannel, data) {
// eslint-disable-next-line no-restricted-globals
using stack = new DisposableStack();
// Enter stores using withScope
if (activeChannel._stores) {
for (const entry of activeChannel._stores.entries()) {
const store = entry[0];
const transform = entry[1];
let newContext = data;
if (transform) {
try {
newContext = transform(data);
} catch (err) {
process.nextTick(() => {
triggerUncaughtException(err, false);
});
continue;
}
}
stack.use(store.withScope(newContext));
}
}
// Publish data
activeChannel.publish(data);
// Transfer ownership of the stack
this.#stack = stack.move();
}
[SymbolDispose]() {
this.#stack[SymbolDispose]();
}
}
// TODO(qard): should there be a C++ channel interface?
class ActiveChannel {
subscribe(subscription) {
validateFunction(subscription, 'subscription');
this._subscribers = ArrayPrototypeSlice(this._subscribers);
ArrayPrototypePush(this._subscribers, subscription);
channels.incRef(this.name);
if (this._index !== undefined) subscriberCounts[this._index]++;
}
unsubscribe(subscription) {
const index = ArrayPrototypeIndexOf(this._subscribers, subscription);
if (index === -1) return false;
const before = ArrayPrototypeSlice(this._subscribers, 0, index);
const after = ArrayPrototypeSlice(this._subscribers, index + 1);
this._subscribers = before;
ArrayPrototypePushApply(this._subscribers, after);
channels.decRef(this.name);
if (this._index !== undefined) subscriberCounts[this._index]--;
maybeMarkInactive(this);
return true;
}
bindStore(store, transform) {
const replacing = this._stores.has(store);
if (!replacing) {
channels.incRef(this.name);
if (this._index !== undefined) subscriberCounts[this._index]++;
}
this._stores.set(store, transform);
}
unbindStore(store) {
if (!this._stores.has(store)) {
return false;
}
this._stores.delete(store);
channels.decRef(this.name);
if (this._index !== undefined) subscriberCounts[this._index]--;
maybeMarkInactive(this);
return true;
}
get hasSubscribers() {
return true;
}
publish(data) {
const subscribers = this._subscribers;
for (let i = 0; i < (subscribers?.length || 0); i++) {
try {
const onMessage = subscribers[i];
onMessage(data, this.name);
} catch (err) {
process.nextTick(() => {
triggerUncaughtException(err, false);
});
}
}
}
withStoreScope(data) {
return new RunStoresScope(this, data);
}
runStores(data, fn, thisArg, ...args) {
// eslint-disable-next-line no-unused-vars
using scope = this.withStoreScope(data);
return ReflectApply(fn, thisArg, args);
}
}
class Channel {
constructor(name) {
this._subscribers = undefined;
this._stores = undefined;
this.name = name;
if (typeof name === 'string') {
this._index = dc_binding.getOrCreateChannelIndex(name);
}
channels.set(name, this);
}
static [SymbolHasInstance](instance) {
const prototype = ObjectGetPrototypeOf(instance);
return prototype === Channel.prototype ||
prototype === ActiveChannel.prototype;
}
subscribe(subscription) {
markActive(this);
this.subscribe(subscription);
}
unsubscribe() {
return false;
}
bindStore(store, transform) {
markActive(this);
this.bindStore(store, transform);
}
unbindStore() {
return false;
}
get hasSubscribers() {
return false;
}
publish() {}
runStores(data, fn, thisArg, ...args) {
return ReflectApply(fn, thisArg, args);
}
withStoreScope() {
// Return no-op disposable for inactive channels
return {
[SymbolDispose]() {},
};
}
}
const channels = new WeakRefMap();
function channel(name) {
const channel = channels.get(name);
if (channel) return channel;
if (typeof name !== 'string' && typeof name !== 'symbol') {
throw new ERR_INVALID_ARG_TYPE('channel', ['string', 'symbol'], name);
}
return new Channel(name);
}
function subscribe(name, subscription) {
return channel(name).subscribe(subscription);
}
function unsubscribe(name, subscription) {
return channel(name).unsubscribe(subscription);
}
function hasSubscribers(name) {
const channel = channels.get(name);
if (!channel) return false;
return channel.hasSubscribers;
}
const boundedEvents = [
'start',
'end',
];
function assertChannel(value, name) {
if (!(value instanceof Channel)) {
throw new ERR_INVALID_ARG_TYPE(name, ['Channel'], value);
}
}
function emitNonThenableWarning(fn) {
process.emitWarning(`tracePromise was called with the function '${fn.name || '<anonymous>'}', ` +
'which returned a non-thenable.');
}
function channelFromMap(nameOrChannels, name, className) {
if (typeof nameOrChannels === 'string') {
return channel(`tracing:${nameOrChannels}:${name}`);
}
if (typeof nameOrChannels === 'object' && nameOrChannels !== null) {
const channel = nameOrChannels[name];
assertChannel(channel, `nameOrChannels.${name}`);
return channel;
}
throw new ERR_INVALID_ARG_TYPE('nameOrChannels',
['string', 'object', className],
nameOrChannels);
}
class BoundedChannelScope {
#context;
#end;
#scope;
constructor(boundedChannel, context) {
// Only proceed if there are subscribers
if (!boundedChannel.hasSubscribers) {
return;
}
const { start, end } = boundedChannel;
this.#context = context;
this.#end = end;
// Use RunStoresScope for the start channel
this.#scope = new RunStoresScope(start, context);
}
[SymbolDispose]() {
if (!this.#scope) {
return;
}
// Publish end event
this.#end.publish(this.#context);
// Dispose the start scope to restore stores
this.#scope[SymbolDispose]();
this.#scope = undefined;
}
}
class BoundedChannel {
constructor(nameOrChannels) {
for (let i = 0; i < boundedEvents.length; ++i) {
const eventName = boundedEvents[i];
ObjectDefineProperty(this, eventName, {
__proto__: null,
value: channelFromMap(nameOrChannels, eventName, 'BoundedChannel'),
});
}
}
get hasSubscribers() {
return this.start?.hasSubscribers ||
this.end?.hasSubscribers;
}
subscribe(handlers) {
for (let i = 0; i < boundedEvents.length; ++i) {
const name = boundedEvents[i];
if (!handlers[name]) continue;
this[name]?.subscribe(handlers[name]);
}
}
unsubscribe(handlers) {
let done = true;
for (let i = 0; i < boundedEvents.length; ++i) {
const name = boundedEvents[i];
if (!handlers[name]) continue;
if (!this[name]?.unsubscribe(handlers[name])) {
done = false;
}
}
return done;
}
withScope(context = kEmptyObject) {
return new BoundedChannelScope(this, context);
}
run(context, fn, thisArg, ...args) {
context ??= {};
// eslint-disable-next-line no-unused-vars
using scope = this.withScope(context);
return ReflectApply(fn, thisArg, args);
}
}
function boundedChannel(nameOrChannels) {
return new BoundedChannel(nameOrChannels);
}
class TracingChannel {
#callWindow;
#continuationWindow;
constructor(nameOrChannels) {
// Create a BoundedChannel for start/end (call window)
if (typeof nameOrChannels === 'string') {
this.#callWindow = new BoundedChannel(nameOrChannels);
this.#continuationWindow = new BoundedChannel({
start: channel(`tracing:${nameOrChannels}:asyncStart`),
end: channel(`tracing:${nameOrChannels}:asyncEnd`),
});
} else if (typeof nameOrChannels === 'object') {
this.#callWindow = new BoundedChannel({
start: nameOrChannels.start,
end: nameOrChannels.end,
});
this.#continuationWindow = new BoundedChannel({
start: nameOrChannels.asyncStart,
end: nameOrChannels.asyncEnd,
});
}
// Create individual channel for error
ObjectDefineProperty(this, 'error', {
__proto__: null,
value: channelFromMap(nameOrChannels, 'error', 'TracingChannel'),
});
}
get start() {
return this.#callWindow.start;
}
get end() {
return this.#callWindow.end;
}
get asyncStart() {
return this.#continuationWindow.start;
}
get asyncEnd() {
return this.#continuationWindow.end;
}
get hasSubscribers() {
return this.#callWindow.hasSubscribers ||
this.#continuationWindow.hasSubscribers ||
this.error?.hasSubscribers;
}
subscribe(handlers) {
// Subscribe to call window (start/end)
if (handlers.start || handlers.end) {
this.#callWindow.subscribe({
start: handlers.start,
end: handlers.end,
});
}
// Subscribe to continuation window (asyncStart/asyncEnd)
if (handlers.asyncStart || handlers.asyncEnd) {
this.#continuationWindow.subscribe({
start: handlers.asyncStart,
end: handlers.asyncEnd,
});
}
// Subscribe to error channel
if (handlers.error) {
this.error.subscribe(handlers.error);
}
}
unsubscribe(handlers) {
let done = true;
// Unsubscribe from call window
if (handlers.start || handlers.end) {
if (!this.#callWindow.unsubscribe({
start: handlers.start,
end: handlers.end,
})) {
done = false;
}
}
// Unsubscribe from continuation window
if (handlers.asyncStart || handlers.asyncEnd) {
if (!this.#continuationWindow.unsubscribe({
start: handlers.asyncStart,
end: handlers.asyncEnd,
})) {
done = false;
}
}
// Unsubscribe from error channel
if (handlers.error) {
if (!this.error.unsubscribe(handlers.error)) {
done = false;
}
}
return done;
}
traceSync(fn, context = { __proto__: null }, thisArg, ...args) {
if (!this.hasSubscribers) {
return ReflectApply(fn, thisArg, args);
}
const { error } = this;
// eslint-disable-next-line no-unused-vars
using scope = this.#callWindow.withScope(context);
try {
const result = ReflectApply(fn, thisArg, args);
context.result = result;
return result;
} catch (err) {
context.error = err;
error.publish(context);
throw err;
}
}
tracePromise(fn, context = { __proto__: null }, thisArg, ...args) {
if (!this.hasSubscribers) {
const result = ReflectApply(fn, thisArg, args);
if (typeof result?.then !== 'function') {
emitNonThenableWarning(fn);
}
return result;
}
const { error } = this;
const continuationWindow = this.#continuationWindow;
function onReject(err) {
context.error = err;
error.publish(context);
// Use continuation window for asyncStart/asyncEnd
// eslint-disable-next-line no-unused-vars
using scope = continuationWindow.withScope(context);
// TODO: Is there a way to have asyncEnd _after_ the continuation?
}
function onRejectWithRethrow(err) {
onReject(err);
throw err;
}
function onResolve(result) {
context.result = result;
// Use continuation window for asyncStart/asyncEnd
// eslint-disable-next-line no-unused-vars
using scope = continuationWindow.withScope(context);
// TODO: Is there a way to have asyncEnd _after_ the continuation?
return result;
}
// eslint-disable-next-line no-unused-vars
using scope = this.#callWindow.withScope(context);
try {
const result = ReflectApply(fn, thisArg, args);
// If the return value is not a thenable, return it directly with a warning.
// Do not publish to asyncStart/asyncEnd.
if (typeof result?.then !== 'function') {
emitNonThenableWarning(fn);
context.result = result;
return result;
}
// isPromise() matches sub-classes, but we need to match only direct
// instances of the native Promise type to safely use PromisePrototypeThen.
if (isPromise(result) && ObjectGetPrototypeOf(result) === PromisePrototype) {
return PromisePrototypeThen(result, onResolve, onRejectWithRethrow);
}
// For non-native thenables, subscribe to the result but return the
// original thenable so the consumer can continue handling it directly.
// Non-native thenables don't have unhandledRejection tracking, so
// swallowing the rejection here doesn't change existing behaviour.
result.then(onResolve, onReject);
return result;
} catch (err) {
context.error = err;
error.publish(context);
throw err;
}
}
traceCallback(fn, position = -1, context = kEmptyObject, thisArg, ...args) {
if (!this.hasSubscribers) {
return ReflectApply(fn, thisArg, args);
}
const { error } = this;
const continuationWindow = this.#continuationWindow;
function wrappedCallback(err, res) {
if (err) {
context.error = err;
error.publish(context);
} else {
context.result = res;
}
// Use continuation window for asyncStart/asyncEnd around callback
// eslint-disable-next-line no-unused-vars
using scope = continuationWindow.withScope(context);
return ReflectApply(callback, this, arguments);
}
const callback = ArrayPrototypeAt(args, position);
validateFunction(callback, 'callback');
ArrayPrototypeSplice(args, position, 1, wrappedCallback);
// eslint-disable-next-line no-unused-vars
using scope = this.#callWindow.withScope(context);
try {
return ReflectApply(fn, thisArg, args);
} catch (err) {
context.error = err;
error.publish(context);
throw err;
}
}
}
function tracingChannel(nameOrChannels) {
return new TracingChannel(nameOrChannels);
}
dc_binding.linkNativeChannel((name) => channel(name));
module.exports = {
channel,
hasSubscribers,
subscribe,
tracingChannel,
unsubscribe,
boundedChannel,
Channel,
BoundedChannel,
};