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

[feature]: Support cancellation #16

Closed
dankeboy36 opened this issue Dec 7, 2023 · 6 comments
Closed

[feature]: Support cancellation #16

dankeboy36 opened this issue Dec 7, 2023 · 6 comments
Assignees
Labels
enhancement New feature or request

Comments

@dankeboy36
Copy link
Contributor

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;
    }
})();
@spoenemann spoenemann added the enhancement New feature or request label Dec 8, 2023
@dhuebner dhuebner self-assigned this Dec 8, 2023
@dhuebner
Copy link
Member

dhuebner commented Dec 8, 2023

@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?

@dankeboy36
Copy link
Contributor Author

dankeboy36 commented Dec 8, 2023

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: microsoft/vscode-languageserver-node#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;

@dhuebner
Copy link
Member

@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.

@dhuebner
Copy link
Member

@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?

@dankeboy36
Copy link
Contributor Author

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?

@dhuebner
Copy link
Member

dhuebner commented Dec 6, 2024

New version v0.5.0 containing the requested improvements was released.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants