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

Use EventTarget rather than the Node "events" module #1188

Merged
merged 6 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
"crelt": "^1.0.5",
"dapjs": "2.2.0",
"dompurify": "^2.3.3",
"events": "^3.3.0",
microbit-robert marked this conversation as resolved.
Show resolved Hide resolved
"file-saver": "^2.0.5",
"framer-motion": "^10.2.4",
"lodash.debounce": "^4.0.8",
Expand Down
122 changes: 122 additions & 0 deletions src/common/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* Copyright (c) 2022 Jonas "DerZade" Schade
*
* SPDX-License-Identifier: MIT
*
* https://github.com/DerZade/typescript-event-target/blob/master/src/TypedEventTarget.ts
*/

/**
* A function that can be passed to the `listener` parameter of {@link TypedEventTarget.addEventListener} and {@link TypedEventTarget.removeEventListener}.
*
* @template M A map of event types to their respective event classes.
* @template T The type of event to listen for (has to be keyof `M`).
*/
export type TypedEventListener<M, T extends keyof M> = (
evt: M[T]
) => void | Promise<void>;

/**
* An object that can be passed to the `listener` parameter of {@link TypedEventTarget.addEventListener} and {@link TypedEventTarget.removeEventListener}.
*
* @template M A map of event types to their respective event classes.
* @template T The type of event to listen for (has to be keyof `M`).
*/
export interface TypedEventListenerObject<M, T extends keyof M> {
handleEvent: (evt: M[T]) => void | Promise<void>;
}

/**
* Type of parameter `listener` in {@link TypedEventTarget.addEventListener} and {@link TypedEventTarget.removeEventListener}.
*
* The object that receives a notification (an object that implements the Event interface) when an event of the specified type occurs.
*
* Can be either an object with a handleEvent() method, or a JavaScript function.
*
* @template M A map of event types to their respective event classes.
* @template T The type of event to listen for (has to be keyof `M`).
*/
export type TypedEventListenerOrEventListenerObject<M, T extends keyof M> =
| TypedEventListener<M, T>
| TypedEventListenerObject<M, T>;

type ValueIsEvent<T> = {
[key in keyof T]: Event;
};

/**
* Typescript friendly version of {@link EventTarget}
*
* @template M A map of event types to their respective event classes.
*
* @example
* ```typescript
* interface MyEventMap {
* hello: Event;
* time: CustomEvent<number>;
* }
*
* const eventTarget = new TypedEventTarget<MyEventMap>();
*
* eventTarget.addEventListener('time', (event) => {
* // event is of type CustomEvent<number>
* });
* ```
*/
export interface TypedEventTarget<M extends ValueIsEvent<M>> {
/** Appends an event listener for events whose type attribute value is type.
* The callback argument sets the callback that will be invoked when the event
* is dispatched.
*
* The options argument sets listener-specific options. For compatibility this
* can be a boolean, in which case the method behaves exactly as if the value
* was specified as options's capture.
*
* When set to true, options's capture prevents callback from being invoked
* when the event's eventPhase attribute value is BUBBLING_PHASE. When false
* (or not present), callback will not be invoked when event's eventPhase
* attribute value is CAPTURING_PHASE. Either way, callback will be invoked if
* event's eventPhase attribute value is AT_TARGET.
*
* When set to true, options's passive indicates that the callback will not
* cancel the event by invoking preventDefault(). This is used to enable
* performance optimizations described in § 2.8 Observing event listeners.
*
* When set to true, options's once indicates that the callback will only be
* invoked once after which the event listener will be removed.
*
* The event listener is appended to target's event listener list and is not
* appended if it has the same type, callback, and capture. */
addEventListener: <T extends keyof M & string>(
type: T,
listener: TypedEventListenerOrEventListenerObject<M, T> | null,
options?: boolean | AddEventListenerOptions
) => void;

/** Removes the event listener in target's event listener list with the same
* type, callback, and options. */
removeEventListener: <T extends keyof M & string>(
type: T,
callback: TypedEventListenerOrEventListenerObject<M, T> | null,
options?: EventListenerOptions | boolean
) => void;

/**
* Dispatches a synthetic event event to target and returns true if either
* event's cancelable attribute value is false or its preventDefault() method
* was not invoked, and false otherwise.
* @deprecated To ensure type safety use `dispatchTypedEvent` instead.
*/
dispatchEvent: (event: Event) => boolean;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
export class TypedEventTarget<M extends ValueIsEvent<M>> extends EventTarget {
/**
* Dispatches a synthetic event event to target and returns true if either
* event's cancelable attribute value is false or its preventDefault() method
* was not invoked, and false otherwise.
*/
public dispatchTypedEvent<T extends keyof M>(_type: T, event: M[T]): boolean {
return super.dispatchEvent(event);
}
}
48 changes: 22 additions & 26 deletions src/device/device-hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,13 @@ import React, {
useEffect,
useState,
} from "react";
import { EVENT_PROJECT_UPDATED, EVENT_TEXT_EDIT } from "../fs/fs";
import { useFileSystem } from "../fs/fs-hooks";
import { useLogging } from "../logging/logging-hooks";
import {
ConnectionStatus,
DeviceConnection,
EVENT_FLASH,
EVENT_SERIAL_DATA,
EVENT_SERIAL_ERROR,
EVENT_SERIAL_RESET,
EVENT_STATUS,
SerialDataEvent,
ConnectionStatusEvent,
} from "./device";
import { SimulatorDeviceConnection } from "./simulator";

Expand Down Expand Up @@ -60,12 +56,12 @@ export const useConnectionStatus = () => {
const device = useDevice();
const [status, setStatus] = useState<ConnectionStatus>(device.status);
useEffect(() => {
const statusListener = (status: ConnectionStatus) => {
setStatus(status);
const statusListener = (event: ConnectionStatusEvent) => {
setStatus(event.status);
};
device.on(EVENT_STATUS, statusListener);
device.addEventListener("status", statusListener);
return () => {
device.removeListener(EVENT_STATUS, statusListener);
device.removeEventListener("status", statusListener);
};
}, [device, setStatus]);

Expand Down Expand Up @@ -189,8 +185,8 @@ export const useDeviceTraceback = () => {

useEffect(() => {
const buffer = new TracebackScrollback();
const dataListener = (data: string) => {
const latest = buffer.push(data);
const dataListener = (event: SerialDataEvent) => {
const latest = buffer.push(event.data);
setRuntimeError((current) => {
if (!current && latest) {
logging.event({
Expand All @@ -204,13 +200,13 @@ export const useDeviceTraceback = () => {
buffer.clear();
setRuntimeError(undefined);
};
device.addListener(EVENT_SERIAL_DATA, dataListener);
device.addListener(EVENT_SERIAL_RESET, clearListener);
device.addListener(EVENT_SERIAL_ERROR, clearListener);
device.addEventListener("serial_data", dataListener);
device.addEventListener("serial_reset", clearListener);
device.addEventListener("serial_error", clearListener);
return () => {
device.removeListener(EVENT_SERIAL_ERROR, clearListener);
device.removeListener(EVENT_SERIAL_RESET, clearListener);
device.removeListener(EVENT_SERIAL_DATA, dataListener);
device.removeEventListener("serial_error", clearListener);
device.removeEventListener("serial_reset", clearListener);
device.removeEventListener("serial_data", dataListener);
};
}, [device, setRuntimeError, logging]);

Expand Down Expand Up @@ -247,15 +243,15 @@ export const DeviceContextProvider = ({
useEffect(() => {
const moveToOutOfSync = () => setSyncStatus(SyncStatus.OUT_OF_SYNC);
const moveToInSync = () => setSyncStatus(SyncStatus.IN_SYNC);
fs.on(EVENT_TEXT_EDIT, moveToOutOfSync);
fs.on(EVENT_PROJECT_UPDATED, moveToOutOfSync);
device.on(EVENT_FLASH, moveToInSync);
device.on(EVENT_STATUS, moveToOutOfSync);
fs.addEventListener("file_text_updated", moveToOutOfSync);
fs.addEventListener("project_updated", moveToOutOfSync);
device.addEventListener("flash", moveToInSync);
device.addEventListener("status", moveToOutOfSync);
return () => {
fs.removeListener(EVENT_TEXT_EDIT, moveToOutOfSync);
fs.removeListener(EVENT_PROJECT_UPDATED, moveToOutOfSync);
device.removeListener(EVENT_STATUS, moveToOutOfSync);
device.removeListener(EVENT_FLASH, moveToInSync);
fs.removeEventListener("file_text_updated", moveToOutOfSync);
fs.removeEventListener("project_updated", moveToOutOfSync);
device.removeEventListener("status", moveToOutOfSync);
device.removeEventListener("flash", moveToInSync);
};
}, [fs, device, setSyncStatus]);
return (
Expand Down
65 changes: 55 additions & 10 deletions src/device/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: MIT
*/
import EventEmitter from "events";
import { TypedEventTarget } from "../common/events";
import { Logging } from "../logging/logging";
import { BoardId } from "./board-id";

Expand Down Expand Up @@ -96,14 +96,6 @@ export enum ConnectionAction {
DISCONNECT = "DISCONNECT",
}

export const EVENT_STATUS = "status";
export const EVENT_SERIAL_DATA = "serial_data";
export const EVENT_SERIAL_RESET = "serial_reset";
export const EVENT_SERIAL_ERROR = "serial_error";
export const EVENT_FLASH = "flash";
export const EVENT_START_USB_SELECT = "start_usb_select";
export const EVENT_END_USB_SELECT = "end_usb_select";

export class HexGenerationError extends Error {}

export interface FlashDataSource {
Expand Down Expand Up @@ -135,7 +127,60 @@ export interface ConnectOptions {

export type BoardVersion = "V1" | "V2";

export interface DeviceConnection extends EventEmitter {
export class ConnectionStatusEvent extends Event {
constructor(public readonly status: ConnectionStatus) {
super("status");
}
}

export class SerialDataEvent extends Event {
constructor(public readonly data: string) {
super("serial_data");
}
}

export class SerialResetEvent extends Event {
constructor() {
super("serial_reset");
}
}

export class SerialErrorEvent extends Event {
constructor(public readonly error: unknown) {
super("serial_error");
}
}

export class FlashEvent extends Event {
constructor() {
super("flash");
}
}

export class StartUSBSelect extends Event {
constructor() {
super("start_usb_select");
}
}

export class EndUSBSelect extends Event {
constructor() {
super("end_usb_select");
}
}

export class DeviceConnectionEventMap {
"status": ConnectionStatusEvent;
"serial_data": SerialDataEvent;
"serial_reset": Event;
"serial_error": Event;
"flash": Event;
"start_usb_select": Event;
"end_usb_select": Event;
}

export interface DeviceConnection
extends TypedEventTarget<DeviceConnectionEventMap> {
status: ConnectionStatus;

/**
Expand Down
Loading