apple2js/js/components/Apple2.tsx
Ian Flanigan 3048ec52e1
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 09:06:58 -07:00

98 lines
3.1 KiB
TypeScript

import { h } from 'preact';
import cs from 'classnames';
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
import { Apple2 as Apple2Impl } from '../apple2';
import Apple2IO from '../apple2io';
import CPU6502 from '../cpu6502';
import { ControlStrip } from './ControlStrip';
import { ErrorModal } from './ErrorModal';
import { Inset } from './Inset';
import { Keyboard } from './Keyboard';
import { Mouse } from './Mouse';
import { Screen } from './Screen';
import { Drives } from './Drives';
import { Slinky } from './Slinky';
import { ThunderClock } from './ThunderClock';
import { spawn, Ready } from './util/promises';
import styles from './css/Apple2.module.css';
/**
* Interface for the Apple2 component.
*/
export interface Apple2Props {
characterRom: string;
enhanced: boolean;
e: boolean;
gl: boolean;
rom: string;
sectors: number;
}
/**
* Component to bind various UI components together to form
* the application layout. Includes the screen, drives,
* emulator controls and keyboard. Bootstraps the core
* Apple2 emulator.
*
* @param props Apple2 initialization props
* @returns
*/
export const Apple2 = (props: Apple2Props) => {
const { e, enhanced, sectors } = props;
const screen = useRef<HTMLCanvasElement>(null);
const [apple2, setApple2] = useState<Apple2Impl>();
const [io, setIO] = useState<Apple2IO>();
const [cpu, setCPU] = useState<CPU6502>();
const [error, setError] = useState<unknown>();
const drivesReady = useMemo(() => new Ready(setError), []);
useEffect(() => {
if (screen.current) {
const options = {
canvas: screen.current,
tick: () => { /* do nothing */ },
...props,
};
const apple2 = new Apple2Impl(options);
const controller = spawn(async (signal) => {
try {
await apple2.ready;
if (signal.aborted) {
return;
}
setApple2(apple2);
setIO(apple2.getIO());
setCPU(apple2.getCPU());
await drivesReady.ready;
if (signal.aborted) {
return;
}
apple2.reset();
apple2.run();
} catch (e) {
setError(e);
}
});
return controller.abort();
}
}, [props, drivesReady]);
return (
<div className={cs(styles.outer, { apple2e: e })}>
<Screen screen={screen} />
<Slinky io={io} slot={2} />
<Mouse cpu={cpu} screen={screen} io={io} slot={4} />
<ThunderClock io={io} slot={5} />
<Inset>
<Drives cpu={cpu} io={io} sectors={sectors} enhanced={enhanced} ready={drivesReady} />
</Inset>
<ControlStrip apple2={apple2} e={e} />
<Inset>
<Keyboard apple2={apple2} e={e} />
</Inset>
<ErrorModal error={error} setError={setError} />
</div>
);
};