Repository navigation
readline: add support for async iteration #23916
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
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,6 +33,7 @@ const { | |
| ERR_INVALID_OPT_VALUE | ||
| } = require('internal/errors').codes; | ||
| const { debug, inherits } = require('util'); | ||
| const { emitExperimentalWarning } = require('internal/util'); | ||
| const { Buffer } = require('buffer'); | ||
| const EventEmitter = require('events'); | ||
| const { | ||
|
|
@@ -54,11 +55,16 @@ const { | |
| // Lazy load StringDecoder for startup performance. | ||
| let StringDecoder; | ||
|
|
||
| // Lazy load Readable for startup performance. | ||
| let Readable; | ||
|
|
||
| const kHistorySize = 30; | ||
| const kMincrlfDelay = 100; | ||
| // \r\n, \n, or \r followed by something other than \n | ||
| const lineEnding = /\r?\n|\r(?!\n)/; | ||
|
|
||
| const kLineObjectStream = Symbol('line object stream'); | ||
|
|
||
| const KEYPRESS_DECODER = Symbol('keypress-decoder'); | ||
| const ESCAPE_DECODER = Symbol('escape-decoder'); | ||
|
|
||
|
|
@@ -190,6 +196,8 @@ function Interface(input, output, completer, terminal) { | |
| self._refreshLine(); | ||
| } | ||
|
|
||
| this[kLineObjectStream] = undefined; | ||
|
|
||
| if (!this.terminal) { | ||
| function onSelfCloseWithoutTerminal() { | ||
| input.removeListener('data', ondata); | ||
|
|
@@ -1019,6 +1027,41 @@ Interface.prototype._ttyWrite = function(s, key) { | |
| } | ||
| }; | ||
|
|
||
| Interface.prototype[Symbol.asyncIterator] = function() { | ||
| emitExperimentalWarning('readline Interface [Symbol.asyncIterator]'); | ||
|
|
||
| if (this[kLineObjectStream] === undefined) { | ||
| if (Readable === undefined) { | ||
| Readable = require('stream').Readable; | ||
| } | ||
| const readable = new Readable({ | ||
| objectMode: true, | ||
| read: () => { | ||
| this.resume(); | ||
| }, | ||
| destroy: (err, cb) => { | ||
| this.off('line', lineListener); | ||
| this.off('close', closeListener); | ||
| this.close(); | ||
| cb(err); | ||
| } | ||
| }); | ||
| const lineListener = (input) => { | ||
| if (!readable.push(input)) { | ||
| this.pause(); | ||
| } | ||
| }; | ||
| const closeListener = () => { | ||
| readable.push(null); | ||
| }; | ||
| this.on('line', lineListener); | ||
| this.on('close', closeListener); | ||
| this[kLineObjectStream] = readable; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it make sense expose this as a separate method? converting to a stream might be an issue for multiple people.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I consider this an implementation detail of |
||
| } | ||
|
|
||
| return this[kLineObjectStream][Symbol.asyncIterator](); | ||
| }; | ||
|
|
||
| /** | ||
| * accepts a readable Stream instance and makes it emit "keypress" events | ||
| */ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| 'use strict'; | ||
|
|
||
| const common = require('../common'); | ||
| const assert = require('assert'); | ||
| const { Readable } = require('stream'); | ||
| const readline = require('readline'); | ||
|
|
||
| const CONTENT = 'content'; | ||
| const TOTAL_LINES = 18; | ||
|
|
||
| (async () => { | ||
| const readable = new Readable({ read() {} }); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looking at this now (to solve another bug) I think this backpressure behaviour is confusing since other consumers can listen to |
||
| readable.push(`${CONTENT}\n`.repeat(TOTAL_LINES)); | ||
|
|
||
| const rli = readline.createInterface({ | ||
| input: readable, | ||
| crlfDelay: Infinity | ||
| }); | ||
|
|
||
| const it = rli[Symbol.asyncIterator](); | ||
| const highWaterMark = it.stream.readableHighWaterMark; | ||
|
|
||
| // For this test to work, we have to queue up more than the number of | ||
| // highWaterMark items in rli. Make sure that is the case. | ||
| assert(TOTAL_LINES > highWaterMark); | ||
|
|
||
| let iterations = 0; | ||
| let readableEnded = false; | ||
| for await (const line of it) { | ||
| assert.strictEqual(readableEnded, false); | ||
|
|
||
| assert.strictEqual(line, CONTENT); | ||
|
|
||
| const expectedPaused = TOTAL_LINES - iterations > highWaterMark; | ||
| assert.strictEqual(readable.isPaused(), expectedPaused); | ||
|
|
||
| iterations += 1; | ||
|
|
||
| // We have to end the input stream asynchronously for back pressure to work. | ||
| // Only end when we have reached the final line. | ||
| if (iterations === TOTAL_LINES) { | ||
| readable.push(null); | ||
| readableEnded = true; | ||
| } | ||
| } | ||
|
|
||
| assert.strictEqual(iterations, TOTAL_LINES); | ||
| })().then(common.mustCall()); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| 'use strict'; | ||
|
|
||
| const common = require('../common'); | ||
| const fs = require('fs'); | ||
| const { join } = require('path'); | ||
| const readline = require('readline'); | ||
| const assert = require('assert'); | ||
|
|
||
| const tmpdir = require('../common/tmpdir'); | ||
| tmpdir.refresh(); | ||
|
|
||
| const filename = join(tmpdir.path, 'test.txt'); | ||
|
|
||
| const testContents = [ | ||
| '', | ||
| '\n', | ||
| 'line 1', | ||
| 'line 1\nline 2 南越国是前203年至前111年存在于岭南地区的一个国家\nline 3\ntrailing', | ||
| 'line 1\nline 2\nline 3 ends with newline\n' | ||
| ]; | ||
|
|
||
| async function testSimpleDestroy() { | ||
| for (const fileContent of testContents) { | ||
| fs.writeFileSync(filename, fileContent); | ||
|
|
||
| const readable = fs.createReadStream(filename); | ||
| const rli = readline.createInterface({ | ||
| input: readable, | ||
| crlfDelay: Infinity | ||
| }); | ||
|
|
||
| const iteratedLines = []; | ||
| for await (const k of rli) { | ||
| iteratedLines.push(k); | ||
| break; | ||
| } | ||
|
|
||
| const expectedLines = fileContent.split('\n'); | ||
| if (expectedLines[expectedLines.length - 1] === '') { | ||
| expectedLines.pop(); | ||
| } | ||
| expectedLines.splice(1); | ||
|
|
||
| assert.deepStrictEqual(iteratedLines, expectedLines); | ||
| } | ||
| } | ||
|
|
||
| async function testMutualDestroy() { | ||
| for (const fileContent of testContents) { | ||
| fs.writeFileSync(filename, fileContent); | ||
|
|
||
| const readable = fs.createReadStream(filename); | ||
| const rli = readline.createInterface({ | ||
| input: readable, | ||
| crlfDelay: Infinity | ||
| }); | ||
|
|
||
| const expectedLines = fileContent.split('\n'); | ||
| if (expectedLines[expectedLines.length - 1] === '') { | ||
| expectedLines.pop(); | ||
| } | ||
| expectedLines.splice(2); | ||
|
|
||
| const iteratedLines = []; | ||
| for await (const k of rli) { | ||
| iteratedLines.push(k); | ||
| for await (const l of rli) { | ||
| iteratedLines.push(l); | ||
| break; | ||
| } | ||
| assert.deepStrictEqual(iteratedLines, expectedLines); | ||
| } | ||
|
|
||
| assert.deepStrictEqual(iteratedLines, expectedLines); | ||
| } | ||
| } | ||
|
|
||
| testSimpleDestroy().then(testMutualDestroy).then(common.mustCall()); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| 'use strict'; | ||
|
|
||
| const common = require('../common'); | ||
| const fs = require('fs'); | ||
| const { join } = require('path'); | ||
| const readline = require('readline'); | ||
| const assert = require('assert'); | ||
|
|
||
| const tmpdir = require('../common/tmpdir'); | ||
| tmpdir.refresh(); | ||
|
|
||
| const filename = join(tmpdir.path, 'test.txt'); | ||
|
|
||
| const testContents = [ | ||
| '', | ||
| '\n', | ||
| 'line 1', | ||
| 'line 1\nline 2 南越国是前203年至前111年存在于岭南地区的一个国家\nline 3\ntrailing', | ||
| 'line 1\nline 2\nline 3 ends with newline\n' | ||
| ]; | ||
|
|
||
| async function testSimple() { | ||
| for (const fileContent of testContents) { | ||
| fs.writeFileSync(filename, fileContent); | ||
|
|
||
| const readable = fs.createReadStream(filename); | ||
| const rli = readline.createInterface({ | ||
| input: readable, | ||
| crlfDelay: Infinity | ||
| }); | ||
|
|
||
| const iteratedLines = []; | ||
| for await (const k of rli) { | ||
| iteratedLines.push(k); | ||
| } | ||
|
|
||
| const expectedLines = fileContent.split('\n'); | ||
| if (expectedLines[expectedLines.length - 1] === '') { | ||
| expectedLines.pop(); | ||
| } | ||
| assert.deepStrictEqual(iteratedLines, expectedLines); | ||
| assert.strictEqual(iteratedLines.join(''), fileContent.replace(/\n/gm, '')); | ||
| } | ||
| } | ||
|
|
||
| async function testMutual() { | ||
| for (const fileContent of testContents) { | ||
| fs.writeFileSync(filename, fileContent); | ||
|
|
||
| const readable = fs.createReadStream(filename); | ||
| const rli = readline.createInterface({ | ||
| input: readable, | ||
| crlfDelay: Infinity | ||
| }); | ||
|
|
||
| const expectedLines = fileContent.split('\n'); | ||
| if (expectedLines[expectedLines.length - 1] === '') { | ||
| expectedLines.pop(); | ||
| } | ||
| const iteratedLines = []; | ||
| let iterated = false; | ||
| for await (const k of rli) { | ||
| // This outer loop should only iterate once. | ||
| assert.strictEqual(iterated, false); | ||
| iterated = true; | ||
|
|
||
| iteratedLines.push(k); | ||
| for await (const l of rli) { | ||
| iteratedLines.push(l); | ||
| } | ||
| assert.deepStrictEqual(iteratedLines, expectedLines); | ||
| } | ||
| assert.deepStrictEqual(iteratedLines, expectedLines); | ||
| } | ||
| } | ||
|
|
||
| testSimple().then(testMutual).then(common.mustCall()); |
Uh oh!
There was an error while loading. Please reload this page.