From fb275c6a90c7b824acd2eadbbf3878c71095d535 Mon Sep 17 00:00:00 2001 From: rochdev Date: Wed, 4 Feb 2026 19:21:09 -0500 Subject: [PATCH 1/4] diagnostics_channel: add iterator support to tracing channel --- lib/diagnostics_channel.js | 80 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/lib/diagnostics_channel.js b/lib/diagnostics_channel.js index 3deb301e7f3cd2..e8edde66e0509a 100644 --- a/lib/diagnostics_channel.js +++ b/lib/diagnostics_channel.js @@ -281,6 +281,8 @@ function tracingChannelFrom(nameOrChannels, name) { } class TracingChannel { + #generatorChannel + constructor(nameOrChannels) { for (let i = 0; i < traceEvents.length; ++i) { const eventName = traceEvents[i]; @@ -428,6 +430,84 @@ class TracingChannel { } }); } + + traceIterator(fn, context = {}, thisArg, ...args) { + if (!this.hasSubscribers) { + return ReflectApply(fn, thisArg, args); + } + + const { channel, start, end, asyncStart, asyncEnd, error } = this; + + if (!this.#nextChannel) { + this.#nextChannel = this.tracingChannel({ + start: channel(start.name.slice(0, -6) + ':next:start'), + end: channel(end.name.slice(0, -4) + ':next:end'), + asyncStart: channel(asyncStart.name.slice(0, -11) + ':next:asyncStart'), + asyncEnd: channel(asyncEnd.name.slice(0, -9) + ':next:asyncEnd'), + error: channel(error.name.slice(0, -6) + ':next:error'), + }); + } + + const iter = this.#traceMaybePromise(fn, context, thisArg, ...args); + + return iter instanceof Promise + ? iter.then((iter, method) => { + const { next: iterNext, return: iterReturn, throw: iterThrow } = iter; + + iter.next = (...args) => + this.#nextChannel.#traceMaybePromise(iterNext, ctx, iter, ...args); + iter.return = (...args) => + this.#nextChannel.#traceMaybePromise(iterReturn, ctx, iter, ...args); + iter.throw = (...args) => + this.#nextChannel.#traceMaybePromise(iterThrow, ctx, iter, ...args); + + return iter; + }) + : iter; + } + + #traceMaybePromise(fn, context = {}, thisArg, ...args) { + if (!this.hasSubscribers) { + return ReflectApply(fn, thisArg, args); + } + + const { start, end, asyncStart, asyncEnd, error } = this; + + function reject(err) { + context.error = err; + error.publish(context); + asyncStart.publish(context); + // TODO: Is there a way to have asyncEnd _after_ the continuation? + asyncEnd.publish(context); + return PromiseReject(err); + } + + function resolve(result) { + context.result = result; + asyncStart.publish(context); + // TODO: Is there a way to have asyncEnd _after_ the continuation? + asyncEnd.publish(context); + return result; + } + + return start.runStores(context, () => { + try { + const result = ReflectApply(fn, thisArg, args); + // TODO: Should tracePromise just always do this? + if (!(result instanceof Promise)) { + context.result = result + return result + } + return PromisePrototypeThen(result, resolve, reject); + } catch (err) { + context.error = err; + error.publish(context); + throw err; + } finally { + end.publish(context); + } + }); + } } function tracingChannel(nameOrChannels) { From ee346bcf48d85923450d19011f50fb817d944aeb Mon Sep 17 00:00:00 2001 From: rochdev Date: Sun, 8 Mar 2026 15:51:08 -0400 Subject: [PATCH 2/4] add tests and fix missing cases --- lib/diagnostics_channel.js | 36 +++++----- ...nel-tracing-channel-iterator-async-next.js | 54 +++++++++++++++ ...nel-tracing-channel-iterator-early-exit.js | 34 ++++++++++ ...-channel-tracing-channel-iterator-error.js | 57 ++++++++++++++++ ...ing-channel-iterator-promise-async-next.js | 67 +++++++++++++++++++ ...hannel-tracing-channel-iterator-promise.js | 55 +++++++++++++++ ...nel-tracing-channel-iterator-run-stores.js | 31 +++++++++ ...ostics-channel-tracing-channel-iterator.js | 44 ++++++++++++ 8 files changed, 362 insertions(+), 16 deletions(-) create mode 100644 test/parallel/test-diagnostics-channel-tracing-channel-iterator-async-next.js create mode 100644 test/parallel/test-diagnostics-channel-tracing-channel-iterator-early-exit.js create mode 100644 test/parallel/test-diagnostics-channel-tracing-channel-iterator-error.js create mode 100644 test/parallel/test-diagnostics-channel-tracing-channel-iterator-promise-async-next.js create mode 100644 test/parallel/test-diagnostics-channel-tracing-channel-iterator-promise.js create mode 100644 test/parallel/test-diagnostics-channel-tracing-channel-iterator-run-stores.js create mode 100644 test/parallel/test-diagnostics-channel-tracing-channel-iterator.js diff --git a/lib/diagnostics_channel.js b/lib/diagnostics_channel.js index e8edde66e0509a..642299a2905fa8 100644 --- a/lib/diagnostics_channel.js +++ b/lib/diagnostics_channel.js @@ -281,7 +281,7 @@ function tracingChannelFrom(nameOrChannels, name) { } class TracingChannel { - #generatorChannel + #nextChannel constructor(nameOrChannels) { for (let i = 0; i < traceEvents.length; ++i) { @@ -436,10 +436,10 @@ class TracingChannel { return ReflectApply(fn, thisArg, args); } - const { channel, start, end, asyncStart, asyncEnd, error } = this; + const { start, end, asyncStart, asyncEnd, error } = this; if (!this.#nextChannel) { - this.#nextChannel = this.tracingChannel({ + this.#nextChannel = tracingChannel({ start: channel(start.name.slice(0, -6) + ':next:start'), end: channel(end.name.slice(0, -4) + ':next:end'), asyncStart: channel(asyncStart.name.slice(0, -11) + ':next:asyncStart'), @@ -448,22 +448,26 @@ class TracingChannel { }); } - const iter = this.#traceMaybePromise(fn, context, thisArg, ...args); + const nextChannel = this.#nextChannel; + + const wrapIter = (iter) => { + const { next: iterNext, return: iterReturn, throw: iterThrow } = iter; - return iter instanceof Promise - ? iter.then((iter, method) => { - const { next: iterNext, return: iterReturn, throw: iterThrow } = iter; + iter.next = (...args) => + nextChannel.#traceMaybePromise(iterNext, context, iter, ...args); + iter.return = (...args) => + nextChannel.#traceMaybePromise(iterReturn, context, iter, ...args); + iter.throw = (...args) => + nextChannel.#traceMaybePromise(iterThrow, context, iter, ...args); + + return iter; + }; - iter.next = (...args) => - this.#nextChannel.#traceMaybePromise(iterNext, ctx, iter, ...args); - iter.return = (...args) => - this.#nextChannel.#traceMaybePromise(iterReturn, ctx, iter, ...args); - iter.throw = (...args) => - this.#nextChannel.#traceMaybePromise(iterThrow, ctx, iter, ...args); + const result = this.#traceMaybePromise(fn, context, thisArg, ...args); - return iter; - }) - : iter; + return result instanceof Promise + ? PromisePrototypeThen(result, wrapIter) + : wrapIter(result); } #traceMaybePromise(fn, context = {}, thisArg, ...args) { diff --git a/test/parallel/test-diagnostics-channel-tracing-channel-iterator-async-next.js b/test/parallel/test-diagnostics-channel-tracing-channel-iterator-async-next.js new file mode 100644 index 00000000000000..38be884e8886bc --- /dev/null +++ b/test/parallel/test-diagnostics-channel-tracing-channel-iterator-async-next.js @@ -0,0 +1,54 @@ +'use strict'; + +const common = require('../common'); +const dc = require('diagnostics_channel'); +const assert = require('assert'); + +const channel = dc.tracingChannel('test'); +const nextChannel = dc.tracingChannel('test:next'); + +const expectedResult = { foo: 'bar' }; +const input = { foo: 'bar' }; +const thisArg = { baz: 'buz' }; + +function check(found) { + assert.deepStrictEqual(found, input); +} + +function checkNextAsync(found) { + check(found); + assert.strictEqual(found.error, undefined); + assert.deepStrictEqual(found.result, { value: expectedResult, done: false }); +} + +// async function* returns an AsyncGenerator synchronously, so no asyncStart/asyncEnd +// for the fn call itself +const handlers = { + start: common.mustCall(check), + end: common.mustCall(check), + asyncStart: common.mustNotCall(), + asyncEnd: common.mustNotCall(), + error: common.mustNotCall(), +}; + +// next() on an AsyncGenerator returns a Promise +const nextHandlers = { + start: common.mustCall(check), + end: common.mustCall(check), + asyncStart: common.mustCall(checkNextAsync), + asyncEnd: common.mustCall(checkNextAsync), + error: common.mustNotCall(), +}; + +channel.subscribe(handlers); +nextChannel.subscribe(nextHandlers); + +const iter = channel.traceIterator(common.mustCall(async function*(value) { + assert.deepStrictEqual(this, thisArg); + yield value; +}), input, thisArg, expectedResult); + +// next() returns a Promise since iter is an AsyncGenerator +iter.next().then(common.mustCall((result) => { + assert.deepStrictEqual(result, { value: expectedResult, done: false }); +})); diff --git a/test/parallel/test-diagnostics-channel-tracing-channel-iterator-early-exit.js b/test/parallel/test-diagnostics-channel-tracing-channel-iterator-early-exit.js new file mode 100644 index 00000000000000..afc5fb9b61cba3 --- /dev/null +++ b/test/parallel/test-diagnostics-channel-tracing-channel-iterator-early-exit.js @@ -0,0 +1,34 @@ +'use strict'; + +const common = require('../common'); +const dc = require('diagnostics_channel'); + +const channel = dc.tracingChannel('test'); +const nextChannel = dc.tracingChannel('test:next'); + +const handlers = { + start: common.mustNotCall(), + end: common.mustNotCall(), + asyncStart: common.mustNotCall(), + asyncEnd: common.mustNotCall(), + error: common.mustNotCall(), +}; + +const nextHandlers = { + start: common.mustNotCall(), + end: common.mustNotCall(), + asyncStart: common.mustNotCall(), + asyncEnd: common.mustNotCall(), + error: common.mustNotCall(), +}; + +// Subscribe after traceIterator call - no events should fire for the iterator +// or for subsequent next() calls since the iterator was not wrapped +const iter = channel.traceIterator(function*() { + yield 1; +}, {}); + +channel.subscribe(handlers); +nextChannel.subscribe(nextHandlers); + +iter.next(); diff --git a/test/parallel/test-diagnostics-channel-tracing-channel-iterator-error.js b/test/parallel/test-diagnostics-channel-tracing-channel-iterator-error.js new file mode 100644 index 00000000000000..0d47acfc7f26fd --- /dev/null +++ b/test/parallel/test-diagnostics-channel-tracing-channel-iterator-error.js @@ -0,0 +1,57 @@ +'use strict'; + +const common = require('../common'); +const dc = require('diagnostics_channel'); +const assert = require('assert'); + +const channel = dc.tracingChannel('test'); +const nextChannel = dc.tracingChannel('test:next'); + +const expectedError = new Error('test'); +const input = { foo: 'bar' }; +const thisArg = { baz: 'buz' }; + +function check(found) { + assert.deepStrictEqual(found, input); +} + +function checkError(found) { + check(found); + assert.deepStrictEqual(found.error, expectedError); +} + +// Two traceIterator calls: one for next() error, one for throw() error +const handlers = { + start: common.mustCall(check, 2), + end: common.mustCall(check, 2), + asyncStart: common.mustNotCall(), + asyncEnd: common.mustNotCall(), + error: common.mustNotCall(), +}; + +// next() and throw() each produce one error +const nextHandlers = { + start: common.mustCall(check, 2), + end: common.mustCall(check, 2), + asyncStart: common.mustNotCall(), + asyncEnd: common.mustNotCall(), + error: common.mustCall(checkError, 2), +}; + +channel.subscribe(handlers); +nextChannel.subscribe(nextHandlers); + +// Test next(): generator throws on first next() call +const iter1 = channel.traceIterator(common.mustCall(function*() { + assert.deepStrictEqual(this, thisArg); + throw expectedError; +}), input, thisArg); + +assert.throws(() => iter1.next(), expectedError); + +// Test throw(): propagates error through the iterator +const iter2 = channel.traceIterator(common.mustCall(function*() { + yield 1; +}), input, thisArg); + +assert.throws(() => iter2.throw(expectedError), expectedError); diff --git a/test/parallel/test-diagnostics-channel-tracing-channel-iterator-promise-async-next.js b/test/parallel/test-diagnostics-channel-tracing-channel-iterator-promise-async-next.js new file mode 100644 index 00000000000000..12f5eedaab1d80 --- /dev/null +++ b/test/parallel/test-diagnostics-channel-tracing-channel-iterator-promise-async-next.js @@ -0,0 +1,67 @@ +'use strict'; + +const common = require('../common'); +const dc = require('diagnostics_channel'); +const assert = require('assert'); + +const channel = dc.tracingChannel('test'); +const nextChannel = dc.tracingChannel('test:next'); + +const expectedResult = { foo: 'bar' }; +const input = { foo: 'bar' }; +const thisArg = { baz: 'buz' }; + +// An async generator that will be returned from the async fn +async function* asyncGen(value) { + yield value; +} +const asyncIter = asyncGen(expectedResult); + +function check(found) { + assert.deepStrictEqual(found, input); +} + +function checkAsync(found) { + check(found); + assert.strictEqual(found.error, undefined); + assert.strictEqual(found.result, asyncIter); +} + +function checkNextAsync(found) { + check(found); + assert.strictEqual(found.error, undefined); + assert.deepStrictEqual(found.result, { value: expectedResult, done: false }); +} + +// async fn returns a Promise, so main channel fires asyncStart/asyncEnd +const handlers = { + start: common.mustCall(check), + end: common.mustCall(check), + asyncStart: common.mustCall(checkAsync), + asyncEnd: common.mustCall(checkAsync), + error: common.mustNotCall(), +}; + +// next() on an AsyncGenerator returns a Promise +const nextHandlers = { + start: common.mustCall(check), + end: common.mustCall(check), + asyncStart: common.mustCall(checkNextAsync), + asyncEnd: common.mustCall(checkNextAsync), + error: common.mustNotCall(), +}; + +channel.subscribe(handlers); +nextChannel.subscribe(nextHandlers); + +// fn is async: returns a Promise resolving to an AsyncGenerator +// traceIterator returns a Promise resolving to the wrapped AsyncGenerator +channel.traceIterator(common.mustCall(async function() { + assert.deepStrictEqual(this, thisArg); + return asyncIter; +}), input, thisArg).then(common.mustCall((iter) => { + // next() on AsyncGenerator returns a Promise + iter.next().then(common.mustCall((result) => { + assert.deepStrictEqual(result, { value: expectedResult, done: false }); + })); +})); diff --git a/test/parallel/test-diagnostics-channel-tracing-channel-iterator-promise.js b/test/parallel/test-diagnostics-channel-tracing-channel-iterator-promise.js new file mode 100644 index 00000000000000..7a0662716c02b9 --- /dev/null +++ b/test/parallel/test-diagnostics-channel-tracing-channel-iterator-promise.js @@ -0,0 +1,55 @@ +'use strict'; + +const common = require('../common'); +const dc = require('diagnostics_channel'); +const assert = require('assert'); + +const channel = dc.tracingChannel('test'); +const nextChannel = dc.tracingChannel('test:next'); + +const expectedResult = { foo: 'bar' }; +const input = { foo: 'bar' }; +const thisArg = { baz: 'buz' }; + +// A sync iterator that will be returned from the async fn +const syncIter = [expectedResult][Symbol.iterator](); + +function check(found) { + assert.deepStrictEqual(found, input); +} + +function checkAsync(found) { + check(found); + assert.strictEqual(found.error, undefined); + assert.strictEqual(found.result, syncIter); +} + +// async fn returns a Promise, so main channel fires asyncStart/asyncEnd +const handlers = { + start: common.mustCall(check), + end: common.mustCall(check), + asyncStart: common.mustCall(checkAsync), + asyncEnd: common.mustCall(checkAsync), + error: common.mustNotCall(), +}; + +// next() on the wrapped sync iterator returns synchronously +const nextHandlers = { + start: common.mustCall(check), + end: common.mustCall(check), + asyncStart: common.mustNotCall(), + asyncEnd: common.mustNotCall(), + error: common.mustNotCall(), +}; + +channel.subscribe(handlers); +nextChannel.subscribe(nextHandlers); + +// fn is async: returns a Promise resolving to a sync iterator +// traceIterator returns a Promise resolving to the wrapped iterator +channel.traceIterator(common.mustCall(async function() { + assert.deepStrictEqual(this, thisArg); + return syncIter; +}), input, thisArg).then(common.mustCall((iter) => { + assert.deepStrictEqual(iter.next(), { value: expectedResult, done: false }); +})); diff --git a/test/parallel/test-diagnostics-channel-tracing-channel-iterator-run-stores.js b/test/parallel/test-diagnostics-channel-tracing-channel-iterator-run-stores.js new file mode 100644 index 00000000000000..9046e422027b2f --- /dev/null +++ b/test/parallel/test-diagnostics-channel-tracing-channel-iterator-run-stores.js @@ -0,0 +1,31 @@ +'use strict'; + +const common = require('../common'); +const { AsyncLocalStorage } = require('async_hooks'); +const dc = require('diagnostics_channel'); +const assert = require('assert'); + +const channel = dc.tracingChannel('test'); +const nextChannel = dc.tracingChannel('test:next'); +const store = new AsyncLocalStorage(); + +const fnContext = { fn: 'context' }; +const nextContext = { next: 'context' }; + +// Bind a store for the fn call (creates the generator object) +channel.start.bindStore(store, common.mustCall(() => fnContext)); + +// Bind a store for each next() call (runs the generator body) +nextChannel.start.bindStore(store, common.mustCall(() => nextContext)); + +assert.strictEqual(store.getStore(), undefined); + +const iter = channel.traceIterator(common.mustCall(function*() { + // Generator body runs during next(), inside nextChannel.start.runStores + assert.deepStrictEqual(store.getStore(), nextContext); + yield 1; +}), {}); + +assert.strictEqual(store.getStore(), undefined); +iter.next(); +assert.strictEqual(store.getStore(), undefined); diff --git a/test/parallel/test-diagnostics-channel-tracing-channel-iterator.js b/test/parallel/test-diagnostics-channel-tracing-channel-iterator.js new file mode 100644 index 00000000000000..1ccd2f9d6b4ee4 --- /dev/null +++ b/test/parallel/test-diagnostics-channel-tracing-channel-iterator.js @@ -0,0 +1,44 @@ +'use strict'; + +const common = require('../common'); +const dc = require('diagnostics_channel'); +const assert = require('assert'); + +const channel = dc.tracingChannel('test'); +const nextChannel = dc.tracingChannel('test:next'); + +const expectedResult = { foo: 'bar' }; +const input = { foo: 'bar' }; +const thisArg = { baz: 'buz' }; + +function check(found) { + assert.deepStrictEqual(found, input); +} + +const handlers = { + start: common.mustCall(check), + end: common.mustCall(check), + asyncStart: common.mustNotCall(), + asyncEnd: common.mustNotCall(), + error: common.mustNotCall(), +}; + +// next() and return() are each called once +const nextHandlers = { + start: common.mustCall(check, 2), + end: common.mustCall(check, 2), + asyncStart: common.mustNotCall(), + asyncEnd: common.mustNotCall(), + error: common.mustNotCall(), +}; + +channel.subscribe(handlers); +nextChannel.subscribe(nextHandlers); + +const iter = channel.traceIterator(common.mustCall(function*(value) { + assert.deepStrictEqual(this, thisArg); + yield value; +}), input, thisArg, expectedResult); + +assert.deepStrictEqual(iter.next(), { value: expectedResult, done: false }); +assert.deepStrictEqual(iter.return(), { value: undefined, done: true }); From 35cf97053649486c70607180f653909eec39f2c8 Mon Sep 17 00:00:00 2001 From: rochdev Date: Sun, 8 Mar 2026 16:02:12 -0400 Subject: [PATCH 3/4] add docs --- doc/api/diagnostics_channel.md | 130 +++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/doc/api/diagnostics_channel.md b/doc/api/diagnostics_channel.md index e9ac279cc62917..afe4cf6f130210 100644 --- a/doc/api/diagnostics_channel.md +++ b/doc/api/diagnostics_channel.md @@ -978,6 +978,136 @@ channels.asyncStart.bindStore(myStore, (data) => { }); ``` +#### `tracingChannel.traceIterator(fn[, context[, thisArg[, ...args]]])` + + + +* `fn` {Function} Iterator or async iterator returning function to wrap a trace + around +* `context` {Object} Shared object to correlate trace events through +* `thisArg` {any} The receiver to be used for the function call +* `...args` {any} Optional arguments to pass to the function +* Returns: {Iterator|AsyncIterator|Promise} The iterator returned by the given + function, or a {Promise} resolving to it if the function is async + +Trace an iterator-returning function call. This will always produce a +[`start` event][] and [`end` event][] around the synchronous portion of the +function execution. If the given function returns a promise (i.e. is an async +function), it will additionally produce an [`asyncStart` event][] and +[`asyncEnd` event][] when the promise resolves to the iterator. + +Each call to `next()`, `return()`, or `throw()` on the returned iterator is +also traced via a sub-channel derived from the tracing channel name by appending +`:next`. For example, if the tracing channel is named `my-channel`, the +sub-channel will be `my-channel:next`. These calls follow the same event +pattern as the outer function call: [`start` event][] and [`end` event][] for +synchronous results, plus [`asyncStart` event][] and [`asyncEnd` event][] if +the method returns a promise (e.g. when iterating an async iterator). An +[`error` event][] is produced if `next()` throws or the iterator method rejects. + +To ensure only correct trace graphs are formed, events will only be published +if subscribers are present prior to starting the trace. Subscriptions which are +added after the trace begins will not receive future events from that trace, +only future traces will be seen. + +```mjs +import diagnostics_channel from 'node:diagnostics_channel'; + +const channels = diagnostics_channel.tracingChannel('my-channel'); + +// Sync function returning a sync iterator. +// Fires start/end on 'my-channel'; fires start/end on 'my-channel:next' +// for each next() call. +for (const value of channels.traceIterator(function*() { + yield 1; + yield 2; +}, { some: 'thing' })) { + // consume values +} + +// Sync call to an async generator function, returning an AsyncIterator. +// Fires start/end on 'my-channel'; fires start/end/asyncStart/asyncEnd on +// 'my-channel:next' for each next() call because next() returns a Promise. +for await (const value of channels.traceIterator(async function*() { + yield 1; + yield 2; +}, { some: 'thing' })) { + // consume values +} + +// Async function returning a sync iterator. +// Fires start/end/asyncStart/asyncEnd on 'my-channel' when the Promise +// resolves; fires start/end on 'my-channel:next' for each next() call. +const iter = await channels.traceIterator(async function() { + return [1, 2].values(); +}, { some: 'thing' }); +for (const value of iter) { + // consume values +} + +// Async function returning an async iterator. +// Fires start/end/asyncStart/asyncEnd on 'my-channel' when the Promise +// resolves; fires start/end/asyncStart/asyncEnd on 'my-channel:next' for each +// next() call. +const asyncIter = await channels.traceIterator(async function() { + return (async function*() { yield 1; yield 2; })(); +}, { some: 'thing' }); +for await (const value of asyncIter) { + // consume values +} +``` + +```cjs +const diagnostics_channel = require('node:diagnostics_channel'); + +const channels = diagnostics_channel.tracingChannel('my-channel'); + +// Sync function returning a sync iterator. +// Fires start/end on 'my-channel'; fires start/end on 'my-channel:next' +// for each next() call. +for (const value of channels.traceIterator(function*() { + yield 1; + yield 2; +}, { some: 'thing' })) { + // consume values +} + +(async () => { + // Sync call to an async generator function, returning an AsyncIterator. + // Fires start/end on 'my-channel'; fires start/end/asyncStart/asyncEnd on + // 'my-channel:next' for each next() call because next() returns a Promise. + for await (const value of channels.traceIterator(async function*() { + yield 1; + yield 2; + }, { some: 'thing' })) { + // consume values + } + + // Async function returning a sync iterator. + // Fires start/end/asyncStart/asyncEnd on 'my-channel' when the Promise + // resolves; fires start/end on 'my-channel:next' for each next() call. + const iter = await channels.traceIterator(async function() { + return [1, 2].values(); + }, { some: 'thing' }); + for (const value of iter) { + // consume values + } + + // Async function returning an async iterator. + // Fires start/end/asyncStart/asyncEnd on 'my-channel' when the Promise + // resolves; fires start/end/asyncStart/asyncEnd on 'my-channel:next' for + // each next() call. + const asyncIter = await channels.traceIterator(async function() { + return (async function*() { yield 1; yield 2; })(); + }, { some: 'thing' }); + for await (const value of asyncIter) { + // consume values + } +})(); +``` + #### `tracingChannel.hasSubscribers`