Skip to content

Commit

Permalink
Refactor(@inquirer/core): Streamline exit handlers (#1542)
Browse files Browse the repository at this point in the history
  • Loading branch information
SBoudrias authored Sep 10, 2024
1 parent ccda936 commit d2690cc
Showing 1 changed file with 38 additions and 48 deletions.
86 changes: 38 additions & 48 deletions packages/core/src/lib/create-prompt.mts
Original file line number Diff line number Diff line change
Expand Up @@ -31,41 +31,25 @@ export function createPrompt<Value, Config>(view: ViewFunction<Value, Config>) {
}) as InquirerReadline;
const screen = new ScreenManager(rl);

const {
promise: rootPromise,
resolve,
reject,
} = PromisePolyfill.withResolver<Value>();
const promise = Object.assign(rootPromise, {
/** @deprecated pass an AbortSignal in the context options instead. See {@link https://github.com/SBoudrias/Inquirer.js#canceling-prompt} */
cancel: () => fail(new CancelPromptError()),
});

function onExit() {
cleanups.forEach((cleanup) => cleanup());

screen.done({ clearContent: Boolean(context?.clearPromptOnDone) });
output.end();
}

function fail(error: unknown) {
onExit();
reject(error);
}
const { promise, resolve, reject } = PromisePolyfill.withResolver<Value>();
/** @deprecated pass an AbortSignal in the context options instead. See {@link https://github.com/SBoudrias/Inquirer.js#canceling-prompt} */
const cancel = () => reject(new CancelPromptError());

if (signal) {
const abort = () => fail(new AbortPromptError({ cause: signal.reason }));
const abort = () => reject(new AbortPromptError({ cause: signal.reason }));
if (signal.aborted) {
abort();
return promise;
return Object.assign(promise, { cancel });
}
signal.addEventListener('abort', abort);
cleanups.add(() => signal.removeEventListener('abort', abort));
}

cleanups.add(
onSignalExit((code, signal) => {
fail(new ExitPromptError(`User force closed the prompt with ${code} ${signal}`));
reject(
new ExitPromptError(`User force closed the prompt with ${code} ${signal}`),
);
}),
);

Expand All @@ -77,48 +61,54 @@ export function createPrompt<Value, Config>(view: ViewFunction<Value, Config>) {
rl.input.on('keypress', checkCursorPos);
cleanups.add(() => rl.input.removeListener('keypress', checkCursorPos));

withHooks(rl, (cycle) => {
const hooksCleanup = AsyncResource.bind(() => {
try {
effectScheduler.clearAll();
} catch (error) {
reject(error);
}
});
cleanups.add(hooksCleanup);

return withHooks(rl, (cycle) => {
// The close event triggers immediately when the user press ctrl+c. SignalExit on the other hand
// triggers after the process is done (which happens after timeouts are done triggering.)
// We triggers the hooks cleanup phase on rl `close` so active timeouts can be cleared.
const hooksCleanup = AsyncResource.bind(() => effectScheduler.clearAll());
rl.on('close', hooksCleanup);
cleanups.add(() => rl.removeListener('close', hooksCleanup));

function done(value: Value) {
// Delay execution to let time to the hookCleanup functions to registers.
setImmediate(() => {
onExit();

// Finally we resolve our promise
resolve(value);
});
}

cycle(() => {
try {
const nextView = view(config, done);
const nextView = view(config, (value) => {
setImmediate(() => resolve(value));
});

const [content, bottomContent] =
typeof nextView === 'string' ? [nextView] : nextView;
screen.render(content, bottomContent);

effectScheduler.run();
} catch (error: unknown) {
fail(error);
reject(error);
}
});
});

return promise;
return Object.assign(
promise
.then(
(answer) => {
effectScheduler.clearAll();
return answer;
},
(error) => {
effectScheduler.clearAll();
throw error;
},
)
// Wait for the promise to settle, then cleanup.
.finally(() => {
cleanups.forEach((cleanup) => cleanup());

screen.done({ clearContent: Boolean(context?.clearPromptOnDone) });
output.end();
})
// Once cleanup is done, let the expose promise resolve/reject to the internal one.
.then(() => promise),
{ cancel },
);
});
};

return prompt;
Expand Down

0 comments on commit d2690cc

Please sign in to comment.