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

readline: add support for async iteration #18904

Conversation

prog1dev
Copy link
Contributor

@prog1dev prog1dev commented Feb 21, 2018

Sync readline API with for-await-of support in readable streams

Resolves #18603

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • documentation is changed or added
  • commit message follows commit guidelines
Affected core subsystem(s)

readline, stream, doc, test

@vsemozhetbyt @mcollina

@nodejs-github-bot nodejs-github-bot added the readline Issues and PRs related to the built-in readline module. label Feb 21, 2018
Copy link
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work, LGTM

@mcollina
Copy link
Member

const file_content = 'abc123\n' +
'南越国是前203年至前111年存在于岭南地区的一个国家\n';

fs.writeFile(filename, file_content);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this really be writeFileSync() to avoid any potential race conditions?

tests().then(common.mustCall());

process.on('exit', function() {
fs.unlinkSync(filename);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't this instead be moved to the common.mustCall() callback?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be removed entirely. Test files do not need to clean up the temp directory. Tests that use the temp directory clean it up themselves at the start of the test with tmpdir.refresh().

Copy link
Contributor Author

@prog1dev prog1dev Feb 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Trott Is that worth to also clean up current fs tests in separate pr? example

@targos targos added the semver-minor PRs that contain new features and should be released in the next minor version. label Feb 21, 2018
let data = '';
for await (const k of rli) {
data += k;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be nice to check how many iterations of the loop there were


const common = require('../common');
const fs = require('fs');
const join = require('path').join;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A nit: destruction?

fs.writeFile(filename, file_content);

async function tests() {
await (async function() {
Copy link
Contributor

@vsemozhetbyt vsemozhetbyt Feb 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A nit: arrow function?

@vsemozhetbyt
Copy link
Contributor

vsemozhetbyt commented Feb 21, 2018

Just a tiny doubt: the readline module works with strings only and users may get used to this (line in the listener argument is always a string). Should we demand from a user to use readable.setEncoding('utf8'); here? Maybe we can do this internally? BTW, the test does not use this call.

@prog1dev
Copy link
Contributor Author

prog1dev commented Feb 22, 2018

@vsemozhetbyt By demand you mean explicitly mention this in documentation?
I can set encoding in Interface.prototype[Symbol.asyncIterator] if you think this is a good thing to do.

lib/readline.js Outdated
@@ -239,6 +241,14 @@ Interface.prototype.setPrompt = function(prompt) {
};


Interface.prototype[Symbol.asyncIterator] = function() {
emitExperimentalWarning('Interface[Symbol.asyncIterator]');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: in a project it may not be clear what Interface is

Copy link
Member

@devsnek devsnek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm with nits

@vsemozhetbyt
Copy link
Contributor

@prog1dev Yes, documented advice.
Let us see what others think about it, I may be wrong.

@vsemozhetbyt
Copy link
Contributor

vsemozhetbyt commented Feb 23, 2018

@prog1dev I've compiled the branch and tested the slightly edited doc example code on Windows 7 x64.

  1. readable.setEncoding('utf8'); seems do not affect the behavior (it is the same with it or without it).
  2. It seems we have only one iteration with many lines glued together:
'use strict';

const readline = require('readline');
const fs = require('fs');

async function processLineByLine(readable) {
  // readable.setEncoding('utf8');
  const rli = readline.createInterface({
    input: readable,
    crlfDelay: Infinity
  });

  let i = 0;

  for await (const line of rli) {
    console.log(`Line ${++i}: ${line}`);
  }
}

processLineByLine(fs.createReadStream(__filename)).catch(console.error);
(node:3148) ExperimentalWarning: Interface[Symbol.asyncIterator] is an experimental feature. This feature could change at any time
Line 1: 'use strict';

const readline = require('readline');
const fs = require('fs');

async function processLineByLine(readable) {
  // readable.setEncoding('utf8');
  const rli = readline.createInterface({
    input: readable,
    crlfDelay: Infinity
  });

  let i = 0;

  for await (const line of rli) {
    console.log(`Line ${i}: ${line}`);
  }
}

processLineByLine(fs.createReadStream(__filename)).catch(console.error);

Dear reviewers, please chime in.

Copy link
Member

@TimothyGu TimothyGu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in @vsemozhetbyt’s example, the added method doesn’t work. readline emits 'line' events that needed to be listened for, not 'data' as is the case for readable streams. As such, the general stream async iterator cannot be used for readline.

The PR just gets an async iterator of the input stream. That completely skips the readline mechanism, and as such does not work.

@prog1dev
Copy link
Contributor Author

prog1dev commented Feb 23, 2018

@mcollina Looks like no matter how much lines file has there is only one iteration of for-await-of. Here is code example:

async function print(readable) {
  readable.setEncoding('utf8');
  let data = '';
  let iterations = 0;
  for await (const k of readable) {
    data += k;
    iterations++;
  }
  console.log(data);
  console.log(iterations);
}

print(fs.createReadStream('file')).catch(console.log);

Is this behaviour intentional? Its different for readable streams in object mode
I guess its because fs.createReadStream('file').read() returns null

@vsemozhetbyt vsemozhetbyt added the experimental Issues and PRs related to experimental features. label Feb 23, 2018
@vsemozhetbyt
Copy link
Contributor

It seems the longing readline to be more like streams has some backing, for example #16178.

TBH, when I started to learn Node.js, using readline for a file processing seemed to me a bit hacky, with all the terminal abundant API to be ignored. And it becomes more awkward with things like crlfDelay: Infinity.

But this is a pretty basic tool for text file processing to be outsourced to more handy userland modules. And this async API would be a big step towards making readline more usable outside the CLI domain till we will have something more concise.

Copy link
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-1 based on @vsemozhetbyt exmaple.

@mcollina
Copy link
Member

Is this behaviour intentional? Its different for readable streams in object mode

I'm pretty sure this is is because of internal buffering. You should try reading a different file. This is an experimental feature, and I would be surprise if it does not have bugs. Feel free to send PRs to fix those.

@BridgeAR
Copy link
Member

BridgeAR commented Mar 2, 2018

Ping @prog1dev

@prog1dev
Copy link
Contributor Author

prog1dev commented Mar 3, 2018

@BridgeAR Im working on this in my spare time

@prog1dev prog1dev force-pushed the readline/add_support_for_async_iteration branch from e3532d8 to 3468ed9 Compare March 12, 2018 20:20
@prog1dev prog1dev force-pushed the readline/add_support_for_async_iteration branch from 716e6f1 to 2f5c558 Compare May 5, 2018 17:21
@vsemozhetbyt
Copy link
Contributor

return null;

return this._buffer.shift();
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this function private?

Copy link
Contributor Author

@prog1dev prog1dev May 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean to rename it to _read?


this._enableBuffer = true;
this.close();
this.input.setEncoding('utf8');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's definitely not super friendly when a module like readline changes stream settings, especially when we only do it with async iteration. We should add this feature without it.

emitExperimentalWarning('readline Interface[Symbol.asyncIterator]');

this._enableBuffer = true;
this.close();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We definitely should not close the readline interface when we start iterating. This will emit events like 'close' that can be very much misleading to users.

Copy link
Contributor Author

@prog1dev prog1dev May 20, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order for it to work stream must be in paused mode so I can get data with stream.read() calls. So instead of closing I guess I can pause it with this.input.pause()

Interface.prototype[Symbol.asyncIterator] = function() {
emitExperimentalWarning('readline Interface[Symbol.asyncIterator]');

this._enableBuffer = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see this ever getting restored after the completion of iteration, and I feel like it should be. Correct me if I'm wrong :)

@TimothyGu
Copy link
Member

There is still a lot of logic shared between ReadableAsyncIterator and ReadlineAsyncIterator, like the entire

      this[kLastResolve] = null;
      this[kLastReject] = null;
      this[kError] = null;
      this[kEnded] = false;
      this[kLastPromise] = null;

chunk in the constructor, and also the this[kHandlePromise] function. BaseAsyncIterator could definitely have some more common code.

@@ -77,6 +80,8 @@ function Interface(input, output, completer, terminal) {
this.isCompletionEnabled = true;
this._sawKeyPress = false;
this._previousKey = null;
this._enableBuffer = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please add a comment on what this does? Also, I would prefer if this was a Symbol instead.

@mcollina
Copy link
Member

mcollina commented May 7, 2018

Let me toy with an idea.. how about we implement this on top of a PassThrough instead?
Basically we listen to the 'line' event and we write to a PassThrough with objectMode: true. Then, we use that to create the AsyncIterator. It seems we are doubling some streams machinery here.

@jasnell
Copy link
Member

jasnell commented Oct 17, 2018

What's the status on this?

@prog1dev
Copy link
Contributor Author

@jasnell Stalled. Maybe later this week I can check this PassThrough idea. Also at that time I could not find an easy way to solve this #18904 (comment) issue

@TimothyGu
Copy link
Member

@prog1dev Would it be okay with you if I were to take over this PR?

@prog1dev
Copy link
Contributor Author

@TimothyGu Sure go on

@TimothyGu TimothyGu added the wip Issues and PRs that are still a work in progress. label Oct 25, 2018
@TimothyGu
Copy link
Member

New PR opened at #23916.

TimothyGu added a commit to TimothyGu/node that referenced this pull request Nov 20, 2018
Co-authored-by: Ivan Filenko <ivan.filenko@protonmail.com>
Fixes: nodejs#18603
Refs: nodejs#18904
Trott pushed a commit to Trott/io.js that referenced this pull request Nov 20, 2018
Co-authored-by: Ivan Filenko <ivan.filenko@protonmail.com>
Fixes: nodejs#18603
Refs: nodejs#18904
PR-URL: nodejs#23916
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Gus Caplan <me@gus.host>
@prog1dev prog1dev closed this Nov 20, 2018
@prog1dev prog1dev deleted the readline/add_support_for_async_iteration branch November 20, 2018 23:46
targos pushed a commit that referenced this pull request Nov 21, 2018
Co-authored-by: Ivan Filenko <ivan.filenko@protonmail.com>
Fixes: #18603
Refs: #18904
PR-URL: #23916
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Gus Caplan <me@gus.host>
rvagg pushed a commit that referenced this pull request Nov 28, 2018
Co-authored-by: Ivan Filenko <ivan.filenko@protonmail.com>
Fixes: #18603
Refs: #18904
PR-URL: #23916
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Gus Caplan <me@gus.host>
refack pushed a commit to refack/node that referenced this pull request Jan 14, 2019
Co-authored-by: Ivan Filenko <ivan.filenko@protonmail.com>
Fixes: nodejs#18603
Refs: nodejs#18904
PR-URL: nodejs#23916
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Gus Caplan <me@gus.host>
BethGriggs pushed a commit that referenced this pull request Apr 17, 2019
Co-authored-by: Ivan Filenko <ivan.filenko@protonmail.com>
Fixes: #18603
Refs: #18904
PR-URL: #23916
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Gus Caplan <me@gus.host>
BethGriggs pushed a commit that referenced this pull request Apr 28, 2019
Co-authored-by: Ivan Filenko <ivan.filenko@protonmail.com>
Fixes: #18603
Refs: #18904
PR-URL: #23916
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Gus Caplan <me@gus.host>
MylesBorins pushed a commit that referenced this pull request May 16, 2019
Co-authored-by: Ivan Filenko <ivan.filenko@protonmail.com>
Fixes: #18603
Refs: #18904
PR-URL: #23916
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Gus Caplan <me@gus.host>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
experimental Issues and PRs related to experimental features. readline Issues and PRs related to the built-in readline module. semver-minor PRs that contain new features and should be released in the next minor version. stalled Issues and PRs that are stalled. wip Issues and PRs that are still a work in progress.
Projects
None yet
Development

Successfully merging this pull request may close these issues.