vscode-messenger
vscode-messenger copied to clipboard
[feature]: Support cancellation
Thanks for the lib ❤️
Support cancellation. Maybe with AbortSignal
. It's available in the browser and from Node.js 15+: https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal#browser_compatibility
import { isAbortError } from 'abort-controller-x';
import { HOST_EXTENSION, RequestType } from 'vscode-messenger-common';
import { Messenger } from 'vscode-messenger-webview';
(async () => {
try {
const messenger: Messenger = undefined;
type Params = Record<string, string>;
const requestType: RequestType<Params, readonly unknown[]> = { method: 'long-running' };
const myParams: Params = { foo: 'bar' };
const abortController = new AbortController();
const { signal } = abortController;
const result = await messenger.sendRequest(requestType, HOST_EXTENSION, myParams, { signal });
return result;
} catch (err) {
if (isAbortError(err)) {
return [];
}
throw err;
}
})();
@dankeboy36 Thanks for nice feature request! To make it clear: you just want to cancel/reject the pending request and not the handler that is running on extension side?
not the handler that is running on extension side?
Preferably the handler at the extension side. Of course, it should be the responsibility of the extension developer to abort the actual work, but the cancellation signal (CancellationToken
?) should arrive on the extension side. Otherwise, it's OK for the webviews but not for the entire VSIX. I do not know if it's possible with this lib. (Maybe, yes: https://github.com/microsoft/vscode-languageserver-node/issues/486.)
Here is my pseudo-example of a useful feature. I wrote it by hand. Maybe it does not even compile.
protocol.ts
:
import type { RequestType } from 'vscode-messenger-common';
export interface SearchParams {
readonly query: string;
}
export interface SearchResult = readonly string[]
export const searchType: RequestType<SearchParams, SearchResult> = { method: 'search' };
extension.ts
:
import { CancellationToken, ExtensionContext } from 'vscode';
import { Messenger } from 'vscode-messenger';
import { searchType } from '@my-app/protocol';
export function activate(context: ExtensionContext) {
const messenger = new Messenger({ ignoreHiddenViews: false, debugLog: true });
// create the webview panel
// register the webview panel to the messenger
// register other commands, views, and disposables
context.subscriptions.push(messenger.onRequest(searchType, async (params, sender, cancellationToken: CancellationToken) => {
const abortController = new AbortController();
const { signal } = abortController;
const toDispose = cancellationToken.onCancellationRequested(abortController => abortController.abort());
const { execa } = await import('execa');
try {
// this is just a made-up example. imagine any service call that can be interrupted.
// https://www.npmjs.com/package/execa#signal
const { stdout } = await execa('grep', ['-rl', params.query], { signal });
return stdout.split('\n');
} catch (err) {
if (isExecaError(err) && err.isCanceled) {
return [];
}
throw err;
} finally {
toDispose.dispose();
}
}));
return messenger.diagnosticApi();
}
App.tsx
:
import { TextField } from '@vscode/webview-ui-toolkit';
import { VSCodeTextField } from '@vscode/webview-ui-toolkit/react';
import { searchType } from '@my-app/protocol';
import { useEffect, useState } from 'react';
import { Virtuoso } from 'react-virtuoso';
import { HOST_EXTENSION } from 'vscode-messenger-common';
import './App.css';
import { vscode } from './utilities/vscode';
function App() {
const [query, setQuery] = useState<string>('')
const [data, setData] = useState<string[]>([])
useEffect(() => {
const abortController = new window.AbortController();
const signal = abortController.signal;
async function runQuery() {
const result = await vscode.messenger.sendRequest(searchType, HOST_EXTENSION, query, { signal });
setData(result)
}
runQuery()
return () => {
if (signal && abortController.abort) {
abortController.abort();
}
};
}, [query])
return (
<main>
<VSCodeTextField value={query} onInput={event => {
if (event.target instanceof TextField) {
setQuery(event.target.value);
}
}}/>
<Virtuoso
data={data}
itemContent={(index, line) => (
<div>
<div>{line}</div>
</div>
)}
/>
</main>
);
}
export default App;
@dankeboy36
Just sending {signal}
with a request will not work, we need a separate communication channel with a built-in NotificationType
to cancel the running handler on the "other" side.
I will draft an API for that next days, stay tuned.
@dankeboy36
Request handler now has an additional parameter cancelIndicator: CancelIndicator
export type RequestHandler<P, R> =
(params: P, sender: MessageParticipant, cancelIndicator: CancelIndicator) => HandlerResult<R>;
CancelIndicator can be asked for the cancelation state or one can set the callback function onCancel
export interface CancelIndicator {
isCanceled(): boolean;
onCancel: ((reason: string) => void) | undefined;
}
On the sender side sendRequest
method now allows to pass a Cancelable
instance which allows to call cancel(reason: string)
. Calling cancel
will then activate the CancelIndicator. The internal pending request will be rejected.
Simple timeout example:
const cancelable = new vscode_messenger_common.Cancelable();
setTimeout(() => cancelable.cancel('Timeout: 1 sec'), 1000);
const colors = await messenger.sendRequest(
{ method: 'availableColor' },
{ type: 'extension' },
'',
cancelable
);
The code is currently on a branch dhuebner/canelPendingRequest-16
, do you have a possibility to try it out locally?
Absolutely, I have been monitoring your changes. Very nice, thank you!
Is there a reason this lib invents its own Cancelable
type instead of AbortSignal
?