Interruptable spawn (#132)
* Add `spawn` as a way of calling promise-returning blocks
This change adds `spawn` which takes a no-argument, promise-returning
function, calls it, and returns `void`. This makes it easy to call
async blocks from `useEffect` and other places that don't take async
functions, but also makes such calls explicit.
* Adds interruptability to `spawn`
Now, the task function passed to `spawn` can take an `Interrupted`
argument, which is merely a method that returns `true` if the task
should stop doing work. Likewise, `spawn` returns an `Interrupt`
function that causes the `Interrupted` function to return `true`.
* Change to using `AbortController` and `AbortSignal`
Before, `spawn` used functions to interrupt and determine interruption
state. Now, based on feedback from @whscullin, it uses
`AbortController` and `AbortSignal`.
Tests now show how the controller can be used to abort long-running
tasks and API calls in the `spawn`. The also show how signals can be
chained using `addEventListener`.
* Fix `Apple2.tsx`
Forgot to change it to use `AbortController` and `AbortSignal`.
Co-authored-by: Will Scullin <scullin@scullin.com>
2022-06-12 16:06:58 +00:00
|
|
|
/** @jest-environment jsdom */
|
|
|
|
|
|
|
|
import { Ready, spawn } from 'js/components/util/promises';
|
|
|
|
|
|
|
|
describe('promises', () => {
|
|
|
|
describe('spawn', () => {
|
|
|
|
it('returns an AbortController', () => {
|
|
|
|
const controller = spawn(() => Promise.resolve(1));
|
|
|
|
expect(controller).not.toBeNull();
|
|
|
|
expect(controller).toBeInstanceOf(AbortController);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('passes an AbortSignal to the target function', () => {
|
|
|
|
let signalCapture: AbortSignal | null = null;
|
|
|
|
spawn((signal) => {
|
|
|
|
signalCapture = signal;
|
|
|
|
return Promise.resolve(1);
|
|
|
|
});
|
|
|
|
expect(signalCapture).not.toBeNull();
|
|
|
|
expect(signalCapture).toBeInstanceOf(AbortSignal);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('has the controller hooked up to the signal', async () => {
|
|
|
|
let isAborted = false;
|
|
|
|
|
|
|
|
const controllerIsAborted = new Ready();
|
|
|
|
const spawnHasRecorded = new Ready();
|
2022-06-12 16:42:01 +00:00
|
|
|
|
Interruptable spawn (#132)
* Add `spawn` as a way of calling promise-returning blocks
This change adds `spawn` which takes a no-argument, promise-returning
function, calls it, and returns `void`. This makes it easy to call
async blocks from `useEffect` and other places that don't take async
functions, but also makes such calls explicit.
* Adds interruptability to `spawn`
Now, the task function passed to `spawn` can take an `Interrupted`
argument, which is merely a method that returns `true` if the task
should stop doing work. Likewise, `spawn` returns an `Interrupt`
function that causes the `Interrupted` function to return `true`.
* Change to using `AbortController` and `AbortSignal`
Before, `spawn` used functions to interrupt and determine interruption
state. Now, based on feedback from @whscullin, it uses
`AbortController` and `AbortSignal`.
Tests now show how the controller can be used to abort long-running
tasks and API calls in the `spawn`. The also show how signals can be
chained using `addEventListener`.
* Fix `Apple2.tsx`
Forgot to change it to use `AbortController` and `AbortSignal`.
Co-authored-by: Will Scullin <scullin@scullin.com>
2022-06-12 16:06:58 +00:00
|
|
|
const controller = spawn(async (signal) => {
|
|
|
|
await controllerIsAborted.ready;
|
|
|
|
isAborted = signal.aborted;
|
|
|
|
spawnHasRecorded.onReady();
|
|
|
|
});
|
2022-06-12 16:42:01 +00:00
|
|
|
|
Interruptable spawn (#132)
* Add `spawn` as a way of calling promise-returning blocks
This change adds `spawn` which takes a no-argument, promise-returning
function, calls it, and returns `void`. This makes it easy to call
async blocks from `useEffect` and other places that don't take async
functions, but also makes such calls explicit.
* Adds interruptability to `spawn`
Now, the task function passed to `spawn` can take an `Interrupted`
argument, which is merely a method that returns `true` if the task
should stop doing work. Likewise, `spawn` returns an `Interrupt`
function that causes the `Interrupted` function to return `true`.
* Change to using `AbortController` and `AbortSignal`
Before, `spawn` used functions to interrupt and determine interruption
state. Now, based on feedback from @whscullin, it uses
`AbortController` and `AbortSignal`.
Tests now show how the controller can be used to abort long-running
tasks and API calls in the `spawn`. The also show how signals can be
chained using `addEventListener`.
* Fix `Apple2.tsx`
Forgot to change it to use `AbortController` and `AbortSignal`.
Co-authored-by: Will Scullin <scullin@scullin.com>
2022-06-12 16:06:58 +00:00
|
|
|
controller.abort();
|
|
|
|
controllerIsAborted.onReady();
|
2022-06-12 16:42:01 +00:00
|
|
|
|
Interruptable spawn (#132)
* Add `spawn` as a way of calling promise-returning blocks
This change adds `spawn` which takes a no-argument, promise-returning
function, calls it, and returns `void`. This makes it easy to call
async blocks from `useEffect` and other places that don't take async
functions, but also makes such calls explicit.
* Adds interruptability to `spawn`
Now, the task function passed to `spawn` can take an `Interrupted`
argument, which is merely a method that returns `true` if the task
should stop doing work. Likewise, `spawn` returns an `Interrupt`
function that causes the `Interrupted` function to return `true`.
* Change to using `AbortController` and `AbortSignal`
Before, `spawn` used functions to interrupt and determine interruption
state. Now, based on feedback from @whscullin, it uses
`AbortController` and `AbortSignal`.
Tests now show how the controller can be used to abort long-running
tasks and API calls in the `spawn`. The also show how signals can be
chained using `addEventListener`.
* Fix `Apple2.tsx`
Forgot to change it to use `AbortController` and `AbortSignal`.
Co-authored-by: Will Scullin <scullin@scullin.com>
2022-06-12 16:06:58 +00:00
|
|
|
await spawnHasRecorded.ready;
|
|
|
|
expect(isAborted).toBe(true);
|
|
|
|
});
|
|
|
|
|
2022-06-12 16:42:01 +00:00
|
|
|
it('allows long-running tasks to be stopped', async () => {
|
Interruptable spawn (#132)
* Add `spawn` as a way of calling promise-returning blocks
This change adds `spawn` which takes a no-argument, promise-returning
function, calls it, and returns `void`. This makes it easy to call
async blocks from `useEffect` and other places that don't take async
functions, but also makes such calls explicit.
* Adds interruptability to `spawn`
Now, the task function passed to `spawn` can take an `Interrupted`
argument, which is merely a method that returns `true` if the task
should stop doing work. Likewise, `spawn` returns an `Interrupt`
function that causes the `Interrupted` function to return `true`.
* Change to using `AbortController` and `AbortSignal`
Before, `spawn` used functions to interrupt and determine interruption
state. Now, based on feedback from @whscullin, it uses
`AbortController` and `AbortSignal`.
Tests now show how the controller can be used to abort long-running
tasks and API calls in the `spawn`. The also show how signals can be
chained using `addEventListener`.
* Fix `Apple2.tsx`
Forgot to change it to use `AbortController` and `AbortSignal`.
Co-authored-by: Will Scullin <scullin@scullin.com>
2022-06-12 16:06:58 +00:00
|
|
|
let isFinished = false;
|
|
|
|
|
|
|
|
const innerReady = new Ready();
|
|
|
|
const innerFinished = new Ready();
|
2022-06-12 16:42:01 +00:00
|
|
|
|
Interruptable spawn (#132)
* Add `spawn` as a way of calling promise-returning blocks
This change adds `spawn` which takes a no-argument, promise-returning
function, calls it, and returns `void`. This makes it easy to call
async blocks from `useEffect` and other places that don't take async
functions, but also makes such calls explicit.
* Adds interruptability to `spawn`
Now, the task function passed to `spawn` can take an `Interrupted`
argument, which is merely a method that returns `true` if the task
should stop doing work. Likewise, `spawn` returns an `Interrupt`
function that causes the `Interrupted` function to return `true`.
* Change to using `AbortController` and `AbortSignal`
Before, `spawn` used functions to interrupt and determine interruption
state. Now, based on feedback from @whscullin, it uses
`AbortController` and `AbortSignal`.
Tests now show how the controller can be used to abort long-running
tasks and API calls in the `spawn`. The also show how signals can be
chained using `addEventListener`.
* Fix `Apple2.tsx`
Forgot to change it to use `AbortController` and `AbortSignal`.
Co-authored-by: Will Scullin <scullin@scullin.com>
2022-06-12 16:06:58 +00:00
|
|
|
const controller = spawn(async (signal) => {
|
|
|
|
innerReady.onReady();
|
|
|
|
let i = 0;
|
|
|
|
while (!signal.aborted) {
|
|
|
|
i++;
|
|
|
|
await tick();
|
|
|
|
}
|
2022-06-12 16:42:01 +00:00
|
|
|
expect(i).toBe(2);
|
Interruptable spawn (#132)
* Add `spawn` as a way of calling promise-returning blocks
This change adds `spawn` which takes a no-argument, promise-returning
function, calls it, and returns `void`. This makes it easy to call
async blocks from `useEffect` and other places that don't take async
functions, but also makes such calls explicit.
* Adds interruptability to `spawn`
Now, the task function passed to `spawn` can take an `Interrupted`
argument, which is merely a method that returns `true` if the task
should stop doing work. Likewise, `spawn` returns an `Interrupt`
function that causes the `Interrupted` function to return `true`.
* Change to using `AbortController` and `AbortSignal`
Before, `spawn` used functions to interrupt and determine interruption
state. Now, based on feedback from @whscullin, it uses
`AbortController` and `AbortSignal`.
Tests now show how the controller can be used to abort long-running
tasks and API calls in the `spawn`. The also show how signals can be
chained using `addEventListener`.
* Fix `Apple2.tsx`
Forgot to change it to use `AbortController` and `AbortSignal`.
Co-authored-by: Will Scullin <scullin@scullin.com>
2022-06-12 16:06:58 +00:00
|
|
|
isFinished = true;
|
|
|
|
innerFinished.onReady();
|
|
|
|
});
|
|
|
|
await innerReady.ready;
|
|
|
|
await tick();
|
|
|
|
controller.abort();
|
|
|
|
await innerFinished.ready;
|
|
|
|
|
|
|
|
expect(isFinished).toBe(true);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('allows nesting via listeners', async () => {
|
|
|
|
let isInnerAborted = false;
|
|
|
|
|
|
|
|
const innerReady = new Ready();
|
|
|
|
const outerReady = new Ready();
|
|
|
|
const abortRecorded = new Ready();
|
|
|
|
|
|
|
|
const controller = spawn(async (signal) => {
|
|
|
|
const innerController = spawn(async (innerSignal) => {
|
|
|
|
await innerReady.ready;
|
|
|
|
isInnerAborted = innerSignal.aborted;
|
|
|
|
abortRecorded.onReady();
|
|
|
|
});
|
|
|
|
// TODO(flan): Chain signal.reason when calling innerController.abort()
|
|
|
|
// once jsdom 19 has wider adoption (currently on 16.6.0). Likewise there
|
|
|
|
// are some subtle problems with signal.addEventListener that should be
|
|
|
|
// addressed by https://github.com/jsdom/jsdom/pull/3347.
|
|
|
|
// signal.addEventListener('abort', () => innerController.abort(signal.reason));
|
|
|
|
signal.addEventListener('abort', () => innerController.abort());
|
|
|
|
await outerReady.ready;
|
|
|
|
});
|
|
|
|
|
|
|
|
// Abort the outer controller, but don't let the outer block run.
|
|
|
|
controller.abort();
|
|
|
|
// Let the inner block run.
|
|
|
|
innerReady.onReady();
|
|
|
|
await abortRecorded.ready;
|
|
|
|
|
|
|
|
// Inner block is aborted.
|
|
|
|
expect(isInnerAborted).toBe(true);
|
|
|
|
|
|
|
|
// Let outer block finish.
|
|
|
|
outerReady.onReady();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
function tick() {
|
|
|
|
return new Promise(resolve => setTimeout(resolve, 0));
|
2022-06-12 16:42:01 +00:00
|
|
|
}
|