Skip to content

Commit

Permalink
Use EventTarget rather than the Node "events" module (#1188)
Browse files Browse the repository at this point in the history
This should amount no meaningful change but is a useful preliminary to extracting the device code for reuse.

Incorporates the only meaningful file from https://github.com/DerZade/typescript-event-target/ (with licence details) to save on the dependency.
  • Loading branch information
microbit-matt-hillsdon authored Jul 9, 2024
1 parent eded0a4 commit 506446c
Show file tree
Hide file tree
Showing 26 changed files with 436 additions and 203 deletions.
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",
"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

0 comments on commit 506446c

Please sign in to comment.