denops.vim icon indicating copy to clipboard operation
denops.vim copied to clipboard

Add a way to interrupt background process running on Deno

Open lambdalisue opened this issue 5 months ago • 1 comments

Cancellation can be challenging to implement, so we aim to provide a method for denops plugins to handle interruptions.

The proposed API could be:

Promise version

import type { Denops as DenopsOrigin } from "https://deno.land/x/[email protected]/mod.ts";
import { delay } from "https://deno.land/[email protected]/async/mod.ts";

export function main(denops: Denops): void {
  denops.dispatcher = {
    start: async () => {
      const { interrupted, done } = denops.interrupted();
      try {
        for (let i = 0; i < 100; i++) {
          await Promise.race([interrupted, denops.cmd(`echo 'Hello ${i}'`)]);
          await Promise.race([interrupted, delay(100)]);
        }
      } catch (e) {
        if (e instanceof Interrupted) {
          await denops.cmd("echo 'Interrupted'");
          return;
        }
        throw e;
      } finally {
        // Clean up interrupted promise
        done();
      }
    },
  };
}

// Assume that this error is thrown from the `signal` when the user invoke `denops#interrupt()`
class Interrupted extends Error {}

// Assume that Denops v6.1 add `interrupted()` method to Denops
type Denops = DenopsOrigin & {
  interrupted: () => { interrupted: Promise<never>; done: () => void };
};

The implementation on denops.vim is easier when we use Promise<never> as a signal but it's not clear for users while users must call done() or whatever to clean up promises for wait.

AbortSignal version

import type { Denops as DenopsOrigin } from "https://deno.land/x/[email protected]/mod.ts";
import { abortable, delay } from "https://deno.land/[email protected]/async/mod.ts";

export function main(denops: Denops): void {
  denops.dispatcher = {
    start: async () => {
      const { signal } = denops.interrupted();
      try {
        for (let i = 0; i < 100; i++) {
          signal.throwIfAborted();
          await abortable(denops.cmd(`echo 'Hello ${i}'`), signal);
          await delay(100, { signal });
        }
      } catch (e) {
        if (e instanceof Interrupted) {
          await denops.cmd("echo 'Interrupted'");
          return;
        }
        throw e;
      }
    },
  };
}

// Assume that this error is thrown from the `signal` when the user invoke `denops#interrupt()`
class Interrupted extends Error {}

// Assume that Denops v6.1 add `interrupted()` method to Denops
type Denops = DenopsOrigin & {
  interrupted: () => { signal: AbortSignal };
};

The implementation on denops.vim is a bit tough when we use AbortSignal as a signal but it seems this version is more Deno native friendly and users don't need to care about resource management.

lambdalisue avatar Jan 07 '24 08:01 lambdalisue