Skip to content

Commit 87efec0

Browse files
authored
fix(cli-repl): defer exit event until evalutation done (#669)
This otherwise messes with cases where the input stream ends while async evaluation is still in progress (e.g. when piping into mongosh).
1 parent 1968706 commit 87efec0

File tree

2 files changed

+40
-0
lines changed

2 files changed

+40
-0
lines changed

packages/cli-repl/src/async-repl.spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { once } from 'events';
66
import chai, { expect } from 'chai';
77
import sinon from 'ts-sinon';
88
import sinonChai from 'sinon-chai';
9+
import { tick } from '../test/repl-helpers';
910
chai.use(sinonChai);
1011

1112
const delay = promisify(setTimeout);
@@ -123,6 +124,25 @@ describe('AsyncRepl', () => {
123124
expect(foundUid).to.be.true;
124125
});
125126

127+
it('delays the "exit" event until after asynchronous evaluation is finished', async() => {
128+
const { input, repl } = createDefaultAsyncRepl();
129+
let exited = false;
130+
repl.on('exit', () => { exited = true; });
131+
132+
let resolve;
133+
repl.context.asyncFn = () => new Promise((res) => { resolve = res; });
134+
135+
input.end('asyncFn()\n');
136+
expect(exited).to.be.false;
137+
138+
await tick();
139+
resolve();
140+
expect(exited).to.be.false;
141+
142+
await tick();
143+
expect(exited).to.be.true;
144+
});
145+
126146
describe('allows handling exceptions from e.g. the writer function', () => {
127147
it('for succesful completions', async() => {
128148
const error = new Error('throwme');

packages/cli-repl/src/async-repl.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,13 @@ export function start(opts: AsyncREPLOptions): REPLServer {
4949
repl.emit(evalStart, { input } as EvalStartEvent);
5050

5151
try {
52+
let exitEventPending = false;
53+
const exitListener = () => { exitEventPending = true; };
54+
let previousExitListeners: any[] = [];
55+
5256
let sigintListener: (() => void) | undefined = undefined;
5357
let previousSigintListeners: any[] = [];
58+
5459
try {
5560
result = await new Promise((resolve, reject) => {
5661
if (breakEvalOnSigint) {
@@ -68,6 +73,13 @@ export function start(opts: AsyncREPLOptions): REPLServer {
6873
repl.once('SIGINT', sigintListener);
6974
}
7075

76+
// The REPL may become over-eager and emit 'exit' events while our
77+
// evaluation is still in progress (because it doesn't expect async
78+
// evaluation). If that happens, defer the event until later.
79+
previousExitListeners = repl.rawListeners('exit');
80+
repl.removeAllListeners('exit');
81+
repl.once('exit', exitListener);
82+
7183
const evalResult = asyncEval(originalEval, input, context, filename);
7284

7385
if (sigintListener !== undefined) {
@@ -84,6 +96,14 @@ export function start(opts: AsyncREPLOptions): REPLServer {
8496
for (const listener of previousSigintListeners) {
8597
repl.on('SIGINT', listener);
8698
}
99+
100+
repl.removeListener('exit', exitListener);
101+
for (const listener of previousExitListeners) {
102+
repl.on('exit', listener);
103+
}
104+
if (exitEventPending) {
105+
process.nextTick(() => repl.emit('exit'));
106+
}
87107
}
88108
} catch (err) {
89109
try {

0 commit comments

Comments
 (0)