Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Readable._construct behaviour contradicts docs #39992

Open
mikuso opened this issue Sep 4, 2021 · 3 comments
Open

Readable._construct behaviour contradicts docs #39992

mikuso opened this issue Sep 4, 2021 · 3 comments
Labels
doc Issues and PRs related to the documentations. stream Issues and PRs related to the stream subsystem.

Comments

@mikuso
Copy link

mikuso commented Sep 4, 2021

Version

v16.8.0

Platform

Microsoft Windows NT 10.0.19043.0 x64

Subsystem

stream

What steps will reproduce the bug?

Returning a promise from Readable._construct has the same effect as calling the callback. This is not documented and may be a bug?

This leads to the following strange behaviour:

const {Readable} = require('stream');

const stream = new Readable({
    async construct(callback) {
        callback();
    },
    read(size) {
        console.log('read', size);
    },
    destroy(err, callback) {
        console.log('destroy', err);
    }
});

Output:

destroy Error [ERR_MULTIPLE_CALLBACK]: Callback called multiple times
    at new NodeError (node:internal/errors:371:5)
    at onConstruct (node:internal/streams/destroy:255:37)
    at processTicksAndRejections (node:internal/process/task_queues:82:21) {
  code: 'ERR_MULTIPLE_CALLBACK'
}

Similarly:

const {Readable} = require('stream');

const stream = new Readable({
    async construct(callback) {
        // don't call the callback
    },
    read(size) {
        console.log('read');
    }
});

stream.read();

Output:

read

How often does it reproduce? Is there a required condition?

Always

What is the expected behavior?

Flow should not proceed to Readable._read until after the callback passed to Readble._construct has been called.

The docs state:

This optional function will be scheduled in the next tick by the stream constructor, delaying any _read() and _destroy() calls until callback is called.

What do you see instead?

  • In the first example, an error is thrown for calling the callback twice (despite only calling it once)
  • In the second example, flow passes to Readable._read despite the callback not being called yet.

Additional information

It is desirable to be able to be able to use await within Readable._construct, but labelling this function as async (and therefore returning a Promise) appears to cause the callback function to be implicitly called (which is sometimes unwanted).

As a workaround, I can return my own Promise from Readable._construct and use its resolve/reject functions in lieu of the callback.

In summary, either this is expected behaviour and the docs are incorrect - or vice versa. It's hard to tell.

@lpinca
Copy link
Member

lpinca commented Sep 4, 2021

It is not made explicit but you either return a promise or call the callback, not both.

@lpinca lpinca added the stream Issues and PRs related to the stream subsystem. label Sep 4, 2021
@ronag ronag added the doc Issues and PRs related to the documentations. label Sep 9, 2021
@ronag
Copy link
Member

ronag commented Sep 9, 2021

Maybe docs could be improved to be more explicit about this.

@zamnuts
Copy link

zamnuts commented Jan 30, 2022

This "feature" was introduced by #34416 (technically via 744a284). It includes both _construct, _transform, _flush, _final, and _destroy. (Lots of duplication in the impl, bump #34723.) So this is generally applicable for Readable, Writable, and Duplex/Transform interfaces.

They all check if the method is a thenable, then loosely callbackifies it. For example, for _construct, this is by means of the internal destroy implementation:

const result = stream._construct(onConstruct);
if (result != null) {
const then = result.then;
if (typeof then === 'function') {
then.call(
result,
function() {
if (!called) {
process.nextTick(onConstruct, null);
}
},
function(err) {
if (!called) {
process.nextTick(onConstruct, err);
}
});
}
}

Unless I'm reading this wrong, I wonder how the Duplex tests are passing (are they?) since they use both async _construct and callback without hitting ERR_MULTIPLE_CALLBACK, e.g.:

async _construct(callback) {
// eslint-disable-next-line no-restricted-syntax
await setTimeout(common.platformTimeout(1));
callback();
}

Aside, I also noticed that @types/node@16 shows _construct():void for both Readable and Writable:

I'm having trouble figuring out of this is a supported feature / public API, or us stream implementers just got lucky. It is not documented anywhere, yet all the issues/commits/PRs I'm finding discuss it like it is an existing/known feature and there is a test suite available.

Maybe docs could be improved to be more explicit about this.

Do you mean it is a signature with a stability index of 2 ("stable")?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
doc Issues and PRs related to the documentations. stream Issues and PRs related to the stream subsystem.
Projects
None yet
Development

No branches or pull requests

4 participants