mirror of
https://github.com/whscullin/apple2js.git
synced 2024-01-12 14:14:38 +00:00
Add drag and drop for disks (#130)
* Add drag and drop for disks * Simplify storage state * Switch to spawn, add abort signal to loads
This commit is contained in:
parent
3048ec52e1
commit
cc46d040ca
@ -45,6 +45,7 @@ export const Apple2 = (props: Apple2Props) => {
|
||||
const [io, setIO] = useState<Apple2IO>();
|
||||
const [cpu, setCPU] = useState<CPU6502>();
|
||||
const [error, setError] = useState<unknown>();
|
||||
const [ready, setReady] = useState(false);
|
||||
const drivesReady = useMemo(() => new Ready(setError), []);
|
||||
|
||||
useEffect(() => {
|
||||
@ -66,6 +67,9 @@ export const Apple2 = (props: Apple2Props) => {
|
||||
setCPU(apple2.getCPU());
|
||||
await drivesReady.ready;
|
||||
if (signal.aborted) {
|
||||
setApple2(undefined);
|
||||
setIO(undefined);
|
||||
setCPU(undefined);
|
||||
return;
|
||||
}
|
||||
apple2.reset();
|
||||
@ -73,13 +77,14 @@ export const Apple2 = (props: Apple2Props) => {
|
||||
} catch (e) {
|
||||
setError(e);
|
||||
}
|
||||
setReady(true);
|
||||
});
|
||||
return controller.abort();
|
||||
return () => controller.abort();
|
||||
}
|
||||
}, [props, drivesReady]);
|
||||
|
||||
return (
|
||||
<div className={cs(styles.outer, { apple2e: e })}>
|
||||
<div className={cs(styles.outer, { apple2e: e, [styles.ready]: ready })}>
|
||||
<Screen screen={screen} />
|
||||
<Slinky io={io} slot={2} />
|
||||
<Mouse cpu={cpu} screen={screen} io={io} slot={4} />
|
||||
|
@ -3,9 +3,11 @@ import { useCallback, useState } from 'preact/hooks';
|
||||
import cs from 'classnames';
|
||||
import SmartPort from '../cards/smartport';
|
||||
import { BlockFileModal } from './BlockFileModal';
|
||||
import { DiskDragTarget } from './DiskDragTarget';
|
||||
import { ErrorModal } from './ErrorModal';
|
||||
|
||||
import styles from './css/BlockDisk.module.css';
|
||||
import { BLOCK_FORMATS } from 'js/formats/types';
|
||||
|
||||
/**
|
||||
* Storage structure for drive state returned via callbacks.
|
||||
@ -20,7 +22,7 @@ export interface BlockDiskData {
|
||||
* Interface for BlockDisk.
|
||||
*/
|
||||
export interface BlockDiskProps extends BlockDiskData {
|
||||
smartPort: SmartPort | undefined;
|
||||
smartPort: SmartPort;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -48,7 +50,13 @@ export const BlockDisk = ({ smartPort, number, on, name }: BlockDiskProps) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.disk}>
|
||||
<DiskDragTarget
|
||||
className={styles.disk}
|
||||
storage={smartPort}
|
||||
drive={number}
|
||||
formats={BLOCK_FORMATS}
|
||||
onError={setError}
|
||||
>
|
||||
<ErrorModal error={error} setError={setError} />
|
||||
<BlockFileModal
|
||||
smartPort={smartPort}
|
||||
@ -69,6 +77,6 @@ export const BlockDisk = ({ smartPort, number, on, name }: BlockDiskProps) => {
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
</DiskDragTarget>
|
||||
);
|
||||
};
|
||||
|
@ -20,7 +20,7 @@ const DISK_TYPES: FilePickerAcceptType[] = [
|
||||
|
||||
interface BlockFileModalProps {
|
||||
isOpen: boolean;
|
||||
smartPort: SmartPort | undefined;
|
||||
smartPort: SmartPort;
|
||||
number: DriveNumber;
|
||||
onClose: (closeBox?: boolean) => void;
|
||||
}
|
||||
@ -37,7 +37,7 @@ export const BlockFileModal = ({ smartPort, number, onClose, isOpen } : BlockFil
|
||||
const doOpen = useCallback(async () => {
|
||||
const hashParts = getHashParts(hash);
|
||||
|
||||
if (smartPort && handles?.length === 1) {
|
||||
if (handles?.length === 1) {
|
||||
hashParts[number] = '';
|
||||
setBusy(true);
|
||||
try {
|
||||
|
97
js/components/DiskDragTarget.tsx
Normal file
97
js/components/DiskDragTarget.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
import { BLOCK_FORMATS, DISK_FORMATS, DriveNumber, MassStorage, NIBBLE_FORMATS } from 'js/formats/types';
|
||||
import { h, JSX, RefObject } from 'preact';
|
||||
import { useEffect, useRef } from 'preact/hooks';
|
||||
import { loadLocalFile } from './util/files';
|
||||
import { spawn } from './util/promises';
|
||||
|
||||
export interface DiskDragTargetProps<T> extends JSX.HTMLAttributes<HTMLDivElement> {
|
||||
storage: MassStorage<T> | undefined;
|
||||
drive?: DriveNumber;
|
||||
formats: typeof NIBBLE_FORMATS
|
||||
| typeof BLOCK_FORMATS
|
||||
| typeof DISK_FORMATS;
|
||||
dropRef?: RefObject<HTMLElement>;
|
||||
onError: (error: unknown) => void;
|
||||
}
|
||||
|
||||
export const DiskDragTarget = ({
|
||||
storage,
|
||||
drive,
|
||||
dropRef,
|
||||
formats,
|
||||
onError,
|
||||
children,
|
||||
...props
|
||||
}: DiskDragTargetProps<unknown>) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const div = dropRef?.current || ref.current;
|
||||
if (div) {
|
||||
const onDragOver = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
const dt = event.dataTransfer;
|
||||
if (dt) {
|
||||
if (Array.from(dt.items).every((item) => item.kind === 'file')) {
|
||||
dt.dropEffect = 'copy';
|
||||
} else {
|
||||
dt.dropEffect = 'none';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onDragEnd = (event: DragEvent) => {
|
||||
const dt = event.dataTransfer;
|
||||
if (dt?.items) {
|
||||
for (let i = 0; i < dt.items.length; i++) {
|
||||
dt.items.remove(i);
|
||||
}
|
||||
} else {
|
||||
dt?.clearData();
|
||||
}
|
||||
};
|
||||
|
||||
const onDrop = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const targetDrive = drive ?? 1; //TODO(whscullin) Maybe pick available drive
|
||||
|
||||
const dt = event.dataTransfer;
|
||||
if (dt?.files.length === 1 && storage) {
|
||||
spawn(async () => {
|
||||
try {
|
||||
await loadLocalFile(storage, formats, targetDrive, dt.files[0]);
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
}
|
||||
});
|
||||
} else if (dt?.files.length === 2 && storage) {
|
||||
spawn(async () => {
|
||||
try {
|
||||
await loadLocalFile(storage, formats, 1, dt.files[0]);
|
||||
await loadLocalFile(storage, formats, 2, dt.files[1]);
|
||||
} catch (e) {
|
||||
onError(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
div.addEventListener('dragover', onDragOver);
|
||||
div.addEventListener('dragend', onDragEnd);
|
||||
div.addEventListener('drop', onDrop);
|
||||
|
||||
return () => {
|
||||
div.removeEventListener('dragover', onDragOver);
|
||||
div.removeEventListener('dragend', onDragEnd);
|
||||
div.removeEventListener('drop', onDrop);
|
||||
};
|
||||
}
|
||||
}, [drive, dropRef, formats, onError, storage]);
|
||||
|
||||
return (
|
||||
<div ref={ref} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -6,6 +6,8 @@ import { ErrorModal } from './ErrorModal';
|
||||
import { FileModal } from './FileModal';
|
||||
|
||||
import styles from './css/DiskII.module.css';
|
||||
import { DiskDragTarget } from './DiskDragTarget';
|
||||
import { NIBBLE_FORMATS } from 'js/formats/types';
|
||||
|
||||
/**
|
||||
* Storage structure for Disk II state returned via callbacks.
|
||||
@ -21,7 +23,7 @@ export interface DiskIIData {
|
||||
* Interface for Disk II component.
|
||||
*/
|
||||
export interface DiskIIProps extends DiskIIData {
|
||||
disk2: Disk2 | undefined;
|
||||
disk2: Disk2;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -50,7 +52,13 @@ export const DiskII = ({ disk2, number, on, name, side }: DiskIIProps) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.disk}>
|
||||
<DiskDragTarget
|
||||
className={styles.disk}
|
||||
storage={disk2}
|
||||
drive={number}
|
||||
formats={NIBBLE_FORMATS}
|
||||
onError={setError}
|
||||
>
|
||||
<FileModal disk2={disk2} number={number} onClose={doClose} isOpen={modalOpen} />
|
||||
<ErrorModal error={error} setError={setError} />
|
||||
<div className={cs(styles.diskLight, { [styles.on]: on })} />
|
||||
@ -60,6 +68,6 @@ export const DiskII = ({ disk2, number, on, name, side }: DiskIIProps) => {
|
||||
<div className={styles.diskLabel}>
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
</DiskDragTarget>
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { h } from 'preact';
|
||||
import { useCallback, useEffect, useState } from 'preact/hooks';
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks';
|
||||
import Disk2, { Callbacks } from '../cards/disk2';
|
||||
import Apple2IO from '../apple2io';
|
||||
import { DiskII, DiskIIData } from './DiskII';
|
||||
@ -8,12 +8,22 @@ import CPU6502 from 'js/cpu6502';
|
||||
import { BlockDisk } from './BlockDisk';
|
||||
import { ErrorModal } from './ErrorModal';
|
||||
import { ProgressModal } from './ProgressModal';
|
||||
import { loadHttpUnknownFile, getHashParts, loadJSON } from './util/files';
|
||||
import { loadHttpUnknownFile, getHashParts, loadJSON, SmartStorageBroker } from './util/files';
|
||||
import { useHash } from './hooks/useHash';
|
||||
import { DriveNumber } from 'js/formats/types';
|
||||
import { Ready } from './util/promises';
|
||||
import { DISK_FORMATS, DriveNumber } from 'js/formats/types';
|
||||
import { spawn, Ready } from './util/promises';
|
||||
|
||||
import styles from './css/Drives.module.css';
|
||||
import { DiskDragTarget } from './DiskDragTarget';
|
||||
|
||||
/**
|
||||
* Storage device storage
|
||||
*/
|
||||
interface StorageDevices {
|
||||
disk2: Disk2;
|
||||
smartPort: SmartPort;
|
||||
smartStorageBroker: SmartStorageBroker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for Drives component.
|
||||
@ -43,12 +53,14 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => {
|
||||
const [current, setCurrent] = useState(0);
|
||||
const [error, setError] = useState<unknown>();
|
||||
const [total, setTotal] = useState(0);
|
||||
const bodyRef = useRef(document.body);
|
||||
const onProgress = useCallback((current: number, total: number) => {
|
||||
setCurrent(current);
|
||||
setTotal(total);
|
||||
}, []);
|
||||
|
||||
const [disk2, setDisk2] = useState<Disk2>();
|
||||
const [storageDevices, setStorageDevices] = useState<StorageDevices>();
|
||||
|
||||
const [data1, setData1] = useState<DiskIIData>({
|
||||
on: false,
|
||||
number: 1,
|
||||
@ -70,28 +82,36 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => {
|
||||
name: 'HD 2'
|
||||
});
|
||||
|
||||
const [smartPort, setSmartPort] = useState<SmartPort>();
|
||||
|
||||
const hash = useHash();
|
||||
|
||||
useEffect(() => {
|
||||
if (smartPort && disk2) {
|
||||
if (storageDevices) {
|
||||
const { smartStorageBroker, disk2 } = storageDevices;
|
||||
const hashParts = getHashParts(hash);
|
||||
const controllers: AbortController[] = [];
|
||||
let loading = 0;
|
||||
for (const drive of [1, 2] as DriveNumber[]) {
|
||||
if (hashParts && hashParts[drive]) {
|
||||
const hashPart = decodeURIComponent(hashParts[drive]);
|
||||
if (hashPart.match(/^https?:/)) {
|
||||
loading++;
|
||||
loadHttpUnknownFile(disk2, smartPort, drive, hashPart, onProgress)
|
||||
.catch((e) => setError(e))
|
||||
.finally(() => {
|
||||
if (--loading === 0) {
|
||||
ready.onReady();
|
||||
}
|
||||
setCurrent(0);
|
||||
setTotal(0);
|
||||
});
|
||||
controllers.push(spawn(async (signal) => {
|
||||
try {
|
||||
await loadHttpUnknownFile(
|
||||
smartStorageBroker,
|
||||
drive,
|
||||
hashPart,
|
||||
signal,
|
||||
onProgress);
|
||||
} catch (e) {
|
||||
setError(e);
|
||||
}
|
||||
if (--loading === 0) {
|
||||
ready.onReady();
|
||||
}
|
||||
setCurrent(0);
|
||||
setTotal(0);
|
||||
}));
|
||||
} else {
|
||||
const url = `/json/disks/${hashPart}.json`;
|
||||
loadJSON(disk2, drive, url).catch((e) => setError(e));
|
||||
@ -101,8 +121,9 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => {
|
||||
if (!loading) {
|
||||
ready.onReady();
|
||||
}
|
||||
return () => controllers.forEach((controller) => controller.abort());
|
||||
}
|
||||
}, [hash, onProgress, disk2, ready, smartPort]);
|
||||
}, [hash, onProgress, ready, storageDevices]);
|
||||
|
||||
useEffect(() => {
|
||||
const setData = [setData1, setData2];
|
||||
@ -140,15 +161,28 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => {
|
||||
if (cpu && io) {
|
||||
const disk2 = new Disk2(io, callbacks, sectors);
|
||||
io.setSlot(6, disk2);
|
||||
setDisk2(disk2);
|
||||
const smartPort = new SmartPort(cpu, smartPortCallbacks, { block: !enhanced });
|
||||
io.setSlot(7, smartPort);
|
||||
setSmartPort(smartPort);
|
||||
|
||||
const smartStorageBroker = new SmartStorageBroker(disk2, smartPort);
|
||||
setStorageDevices({ disk2, smartPort, smartStorageBroker });
|
||||
}
|
||||
}, [cpu, enhanced, io, sectors]);
|
||||
|
||||
if (!storageDevices) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { disk2, smartPort, smartStorageBroker } = storageDevices;
|
||||
|
||||
return (
|
||||
<div className={styles.drives}>
|
||||
<DiskDragTarget
|
||||
storage={smartStorageBroker}
|
||||
dropRef={bodyRef}
|
||||
className={styles.drives}
|
||||
onError={setError}
|
||||
formats={DISK_FORMATS}
|
||||
>
|
||||
<ProgressModal current={current} total={total} title="Loading..." />
|
||||
<ErrorModal error={error} setError={setError} />
|
||||
<div className={styles.driveBay}>
|
||||
@ -159,6 +193,6 @@ export const Drives = ({ cpu, io, sectors, enhanced, ready }: DrivesProps) => {
|
||||
<BlockDisk smartPort={smartPort} {...smartData1} />
|
||||
<BlockDisk smartPort={smartPort} {...smartData2} />
|
||||
</div>
|
||||
</div>
|
||||
</DiskDragTarget>
|
||||
);
|
||||
};
|
||||
|
@ -43,7 +43,7 @@ export type NibbleFileCallback = (
|
||||
|
||||
interface FileModalProps {
|
||||
isOpen: boolean;
|
||||
disk2: DiskII | undefined;
|
||||
disk2: DiskII;
|
||||
number: DriveNumber;
|
||||
onClose: (closeBox?: boolean) => void;
|
||||
}
|
||||
@ -64,11 +64,11 @@ export const FileModal = ({ disk2, number, onClose, isOpen }: FileModalProps) =>
|
||||
setBusy(true);
|
||||
|
||||
try {
|
||||
if (disk2 && handles?.length === 1) {
|
||||
if (handles?.length === 1) {
|
||||
hashParts[number] = '';
|
||||
await loadLocalNibbleFile(disk2, number, await handles[0].getFile());
|
||||
}
|
||||
if (disk2 && filename) {
|
||||
if (filename) {
|
||||
const name = filename.match(/\/([^/]+).json$/) || ['', ''];
|
||||
hashParts[number] = name[1];
|
||||
await loadJSON(disk2, number, filename);
|
||||
|
@ -1,4 +1,9 @@
|
||||
.outer {
|
||||
margin: auto;
|
||||
width: 620px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.outer.ready {
|
||||
display: block;
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ export const getNameAndExtension = (url: string) => {
|
||||
|
||||
export const loadLocalFile = (
|
||||
storage: MassStorage<NibbleFormat|BlockFormat>,
|
||||
formats: typeof NIBBLE_FORMATS | typeof BLOCK_FORMATS,
|
||||
formats: typeof NIBBLE_FORMATS | typeof BLOCK_FORMATS | typeof DISK_FORMATS,
|
||||
number: DriveNumber,
|
||||
file: File,
|
||||
) => {
|
||||
@ -54,10 +54,7 @@ export const loadLocalFile = (
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = function () {
|
||||
const result = this.result as ArrayBuffer;
|
||||
const parts = file.name.split('.');
|
||||
const ext = parts.pop()?.toLowerCase() || '[none]';
|
||||
const name = parts.join('.');
|
||||
|
||||
const { name, ext } = getNameAndExtension(file.name);
|
||||
if (includes(formats, ext)) {
|
||||
initGamepad();
|
||||
if (storage.setBinary(number, name, ext, result)) {
|
||||
@ -128,9 +125,10 @@ export const loadJSON = async (
|
||||
|
||||
export const loadHttpFile = async (
|
||||
url: string,
|
||||
onProgress?: ProgressCallback
|
||||
signal?: AbortSignal,
|
||||
onProgress?: ProgressCallback,
|
||||
): Promise<ArrayBuffer> => {
|
||||
const response = await fetch(url);
|
||||
const response = await fetch(url, signal ? { signal } : {});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Error loading: ${response.statusText}`);
|
||||
}
|
||||
@ -175,13 +173,14 @@ export const loadHttpBlockFile = async (
|
||||
smartPort: SmartPort,
|
||||
number: DriveNumber,
|
||||
url: string,
|
||||
signal?: AbortSignal,
|
||||
onProgress?: ProgressCallback
|
||||
): Promise<boolean> => {
|
||||
const { name, ext } = getNameAndExtension(url);
|
||||
if (!includes(BLOCK_FORMATS, ext)) {
|
||||
throw new Error(`Extension "${ext}" not recognized.`);
|
||||
}
|
||||
const data = await loadHttpFile(url, onProgress);
|
||||
const data = await loadHttpFile(url, signal, onProgress);
|
||||
smartPort.setBinary(number, name, ext, data);
|
||||
initGamepad();
|
||||
|
||||
@ -202,6 +201,7 @@ export const loadHttpNibbleFile = async (
|
||||
disk2: Disk2,
|
||||
number: DriveNumber,
|
||||
url: string,
|
||||
signal?: AbortSignal,
|
||||
onProgress?: ProgressCallback
|
||||
) => {
|
||||
if (url.endsWith('.json')) {
|
||||
@ -211,34 +211,43 @@ export const loadHttpNibbleFile = async (
|
||||
if (!includes(NIBBLE_FORMATS, ext)) {
|
||||
throw new Error(`Extension "${ext}" not recognized.`);
|
||||
}
|
||||
const data = await loadHttpFile(url, onProgress);
|
||||
const data = await loadHttpFile(url, signal, onProgress);
|
||||
disk2.setBinary(number, name, ext, data);
|
||||
initGamepad();
|
||||
return loadHttpFile(url, onProgress);
|
||||
return loadHttpFile(url, signal, onProgress);
|
||||
};
|
||||
|
||||
export const loadHttpUnknownFile = async (
|
||||
disk2: Disk2,
|
||||
smartPort: SmartPort,
|
||||
smartStorageBroker: SmartStorageBroker,
|
||||
number: DriveNumber,
|
||||
url: string,
|
||||
signal?: AbortSignal,
|
||||
onProgress?: ProgressCallback,
|
||||
) => {
|
||||
const data = await loadHttpFile(url, onProgress);
|
||||
const data = await loadHttpFile(url, signal, onProgress);
|
||||
const { name, ext } = getNameAndExtension(url);
|
||||
if (includes(DISK_FORMATS, ext)) {
|
||||
if (data.byteLength >= 800 * 1024) {
|
||||
if (includes(BLOCK_FORMATS, ext)) {
|
||||
smartPort.setBinary(number, name, ext, data);
|
||||
smartStorageBroker.setBinary(number, name, ext, data);
|
||||
};
|
||||
|
||||
export class SmartStorageBroker implements MassStorage<unknown> {
|
||||
constructor(private disk2: Disk2, private smartPort: SmartPort) {}
|
||||
|
||||
setBinary(drive: DriveNumber, name: string, ext: string, data: ArrayBuffer): boolean {
|
||||
if (includes(DISK_FORMATS, ext)) {
|
||||
if (data.byteLength >= 800 * 1024) {
|
||||
if (includes(BLOCK_FORMATS, ext)) {
|
||||
this.smartPort.setBinary(drive, name, ext, data);
|
||||
} else {
|
||||
throw new Error(`Unable to load "${name}"`);
|
||||
}
|
||||
} else if (includes(NIBBLE_FORMATS, ext)) {
|
||||
this.disk2.setBinary(drive, name, ext, data);
|
||||
} else {
|
||||
throw new Error(`Unable to load "${name}"`);
|
||||
}
|
||||
} else if (includes(NIBBLE_FORMATS, ext)) {
|
||||
disk2.setBinary(number, name, ext, data);
|
||||
} else {
|
||||
throw new Error(`Unable to load "${name}"`);
|
||||
throw new Error(`Extension "${ext}" not recognized.`);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Extension "${ext}" not recognized.`);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -25,26 +25,26 @@ describe('promises', () => {
|
||||
|
||||
const controllerIsAborted = new Ready();
|
||||
const spawnHasRecorded = new Ready();
|
||||
|
||||
|
||||
const controller = spawn(async (signal) => {
|
||||
await controllerIsAborted.ready;
|
||||
isAborted = signal.aborted;
|
||||
spawnHasRecorded.onReady();
|
||||
});
|
||||
|
||||
|
||||
controller.abort();
|
||||
controllerIsAborted.onReady();
|
||||
|
||||
|
||||
await spawnHasRecorded.ready;
|
||||
expect(isAborted).toBe(true);
|
||||
});
|
||||
|
||||
it('allows long-runing tasks to be stopped', async () => {
|
||||
it('allows long-running tasks to be stopped', async () => {
|
||||
let isFinished = false;
|
||||
|
||||
const innerReady = new Ready();
|
||||
const innerFinished = new Ready();
|
||||
|
||||
|
||||
const controller = spawn(async (signal) => {
|
||||
innerReady.onReady();
|
||||
let i = 0;
|
||||
@ -52,8 +52,8 @@ describe('promises', () => {
|
||||
i++;
|
||||
await tick();
|
||||
}
|
||||
expect(i).toBe(2);
|
||||
isFinished = true;
|
||||
console.log(i);
|
||||
innerFinished.onReady();
|
||||
});
|
||||
await innerReady.ready;
|
||||
@ -103,4 +103,4 @@ describe('promises', () => {
|
||||
|
||||
function tick() {
|
||||
return new Promise(resolve => setTimeout(resolve, 0));
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user