feat: add patch to add generic types to eventemitter2

EventEmitter2 has types, but they're very basic and not very type safe.
I created this patch, because my improved types haven't been merged into the official package.

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2023-02-06 13:21:58 +01:00
parent 11c2f57e4b
commit caa53e3556
16 changed files with 445 additions and 22 deletions

View file

@ -0,0 +1,401 @@
diff --git a/eventemitter2.d.ts b/eventemitter2.d.ts
index 230825bc353849ffbc46a886099558bed9d42113..19405bde859cf454333ed08d0a2a7543291e9258 100644
--- a/eventemitter2.d.ts
+++ b/eventemitter2.d.ts
@@ -1,27 +1,24 @@
-export type event = (symbol|string);
-export type eventNS = string|event[];
-
export interface ConstructorOptions {
/**
* @default false
* @description set this to `true` to use wildcards.
*/
- wildcard?: boolean,
+ wildcard?: boolean
/**
* @default '.'
* @description the delimiter used to segment namespaces.
*/
- delimiter?: string,
+ delimiter?: string
/**
* @default false
* @description set this to `true` if you want to emit the newListener events.
*/
- newListener?: boolean,
+ newListener?: boolean
/**
* @default false
* @description set this to `true` if you want to emit the removeListener events.
*/
- removeListener?: boolean,
+ removeListener?: boolean
/**
* @default 10
* @description the maximum amount of listeners that can be assigned to an event.
@@ -38,39 +35,61 @@ export interface ConstructorOptions {
*/
ignoreErrors?: boolean
}
-export interface ListenerFn {
- (...values: any[]): void;
-}
-export interface EventAndListener {
- (event: string | string[], ...values: any[]): void;
+
+export type event = symbol | string
+export type eventNS = string | event[]
+
+export type EventMap = Record<event, (...values: any[]) => void>
+
+export type ListenerFn<
+ EventName extends keyof Events = event,
+ Events extends EventMap = EventMap
+> = Events[EventName]
+
+export interface EventAndListener<
+ EventName extends keyof Events = event,
+ Events extends EventMap = EventMap
+> {
+ (
+ event: EventName,
+ ...values: ListenerFunctionParameters<EventName, Events>
+ ): void
}
-export interface WaitForFilter { (...values: any[]): boolean }
+export interface WaitForFilter<
+ EventName extends keyof Events = event,
+ Events extends EventMap = EventMap
+> {
+ (...values: ListenerFunctionParameters<EventName, Events>): boolean
+}
-export interface WaitForOptions {
+export interface WaitForOptions<
+ EventName extends keyof Events = event,
+ Events extends EventMap = EventMap
+> {
/**
* @default 0
*/
- timeout: number,
+ timeout: number
/**
* @default null
*/
- filter: WaitForFilter,
+ filter: WaitForFilter<EventName, Events>
/**
* @default false
*/
- handleError: boolean,
+ handleError: boolean
/**
* @default Promise
*/
- Promise: Function,
+ Promise: () => void
/**
* @default false
*/
overload: boolean
}
-export interface CancelablePromise<T> extends Promise<T>{
+export interface CancelablePromise<T> extends Promise<T> {
cancel(reason: string): undefined
}
@@ -78,79 +97,236 @@ export interface OnceOptions {
/**
* @default 0
*/
- timeout: number,
+ timeout: number
/**
* @default Promise
*/
- Promise: Function,
+ Promise: () => void
/**
* @default false
*/
overload: boolean
}
-export interface ListenToOptions {
- on?: { (event: event | eventNS, handler: ListenerFn): void },
- off?: { (event: event | eventNS, handler: ListenerFn): void },
- reducers: Function | Object
+export interface ListenToOptions<
+ EventName extends keyof Events = event,
+ Events extends EventMap = EventMap
+> {
+ on?: {
+ (event: EventName | eventNS, handler: ListenerFn<EventName, Events>): void
+ }
+ off?: {
+ (event: EventName | eventNS, handler: ListenerFn<EventName, Events>): void
+ }
+ reducers: () => void | Record<string | number | symbol, unknown>
}
-export interface GeneralEventEmitter{
- addEventListener(event: event, handler: ListenerFn): this,
- removeEventListener(event: event, handler: ListenerFn): this,
- addListener?(event: event, handler: ListenerFn): this,
- removeListener?(event: event, handler: ListenerFn): this,
- on?(event: event, handler: ListenerFn): this,
- off?(event: event, handler: ListenerFn): this
+export interface GeneralEventEmitter<
+ EventName extends keyof Events = event,
+ Events extends EventMap = EventMap
+> {
+ addEventListener(
+ event: EventName,
+ handler: ListenerFn<EventName, Events>
+ ): this
+
+ removeEventListener(
+ event: EventName,
+ handler: ListenerFn<EventName, Events>
+ ): this
+
+ addListener?(event: EventName, handler: ListenerFn<EventName, Events>): this
+
+ removeListener?(
+ event: EventName,
+ handler: ListenerFn<EventName, Events>
+ ): this
+
+ on?(event: EventName, handler: ListenerFn<EventName, Events>): this
+
+ off?(event: EventName, handler: ListenerFn<EventName, Events>): this
}
export interface OnOptions {
- async?: boolean,
- promisify?: boolean,
- nextTick?: boolean,
+ async?: boolean
+ promisify?: boolean
+ nextTick?: boolean
objectify?: boolean
}
-export interface Listener {
- emitter: EventEmitter2;
- event: event|eventNS;
- listener: ListenerFn;
- off(): this;
+export interface Listener<
+ EventName extends keyof Events = event,
+ Events extends EventMap = EventMap
+> {
+ emitter: EventEmitter2<Events>
+ event: EventName | eventNS
+ listener: ListenerFn<EventName, Events>
+
+ off(): this
}
-export declare class EventEmitter2 {
+export type ListenerFunctionParameters<
+ EventName extends keyof Events = event,
+ Events extends EventMap = EventMap
+> = Parameters<Events[EventName]>
+
+export class EventEmitter2<Events extends EventMap = EventMap> {
constructor(options?: ConstructorOptions)
- emit(event: event | eventNS, ...values: any[]): boolean;
- emitAsync(event: event | eventNS, ...values: any[]): Promise<any[]>;
- addListener(event: event | eventNS, listener: ListenerFn): this|Listener;
- on(event: event | eventNS, listener: ListenerFn, options?: boolean|OnOptions): this|Listener;
- prependListener(event: event | eventNS, listener: ListenerFn, options?: boolean|OnOptions): this|Listener;
- once(event: event | eventNS, listener: ListenerFn, options?: true|OnOptions): this|Listener;
- prependOnceListener(event: event | eventNS, listener: ListenerFn, options?: boolean|OnOptions): this|Listener;
- many(event: event | eventNS, timesToListen: number, listener: ListenerFn, options?: boolean|OnOptions): this|Listener;
- prependMany(event: event | eventNS, timesToListen: number, listener: ListenerFn, options?: boolean|OnOptions): this|Listener;
- onAny(listener: EventAndListener): this;
- prependAny(listener: EventAndListener): this;
- offAny(listener: ListenerFn): this;
- removeListener(event: event | eventNS, listener: ListenerFn): this;
- off(event: event | eventNS, listener: ListenerFn): this;
- removeAllListeners(event?: event | eventNS): this;
- setMaxListeners(n: number): void;
- getMaxListeners(): number;
- eventNames(nsAsArray?: boolean): (event|eventNS)[];
- listenerCount(event?: event | eventNS): number
- listeners(event?: event | eventNS): ListenerFn[]
- listenersAny(): ListenerFn[]
- waitFor(event: event | eventNS, timeout?: number): CancelablePromise<any[]>
- waitFor(event: event | eventNS, filter?: WaitForFilter): CancelablePromise<any[]>
- waitFor(event: event | eventNS, options?: WaitForOptions): CancelablePromise<any[]>
- listenTo(target: GeneralEventEmitter, events: event | eventNS, options?: ListenToOptions): this;
- listenTo(target: GeneralEventEmitter, events: event[], options?: ListenToOptions): this;
- listenTo(target: GeneralEventEmitter, events: Object, options?: ListenToOptions): this;
- stopListeningTo(target?: GeneralEventEmitter, event?: event | eventNS): Boolean;
- hasListeners(event?: String): Boolean
- static once(emitter: EventEmitter2, event: event | eventNS, options?: OnceOptions): CancelablePromise<any[]>;
- static defaultMaxListeners: number;
+
+ emit<EventName extends keyof Events>(
+ event: EventName,
+ ...values: ListenerFunctionParameters<EventName, Events>
+ ): boolean
+
+ emit(event: eventNS, ...values: unknown[]): boolean
+
+ emitAsync<EventName extends keyof Events>(
+ event: EventName | eventNS,
+ ...values: ListenerFunctionParameters<EventName, Events>
+ ): Promise<ReturnType<ListenerFn<EventName, Events>>[]>
+
+ addListener<EventName extends keyof Events>(
+ event: EventName | eventNS,
+ listener: ListenerFn<EventName, Events>
+ ): this | Listener<EventName, Events>
+
+ on<EventName extends keyof Events>(
+ event: EventName,
+ listener: ListenerFn<EventName, Events>,
+ options?: boolean | OnOptions
+ ): this | Listener<EventName, Events>
+
+ on(
+ event: eventNS,
+ listener: () => void,
+ options?: boolean | OnOptions
+ ): this | Listener<string, Events>
+
+ prependListener<EventName extends keyof Events>(
+ event: EventName | eventNS,
+ listener: ListenerFn<EventName, Events>,
+ options?: boolean | OnOptions
+ ): this | Listener<EventName, Events>
+
+ once<EventName extends keyof Events>(
+ event: EventName | eventNS,
+ listener: ListenerFn<EventName, Events>,
+ options?: true | OnOptions
+ ): this | Listener<EventName, Events>
+
+ prependOnceListener<EventName extends keyof Events>(
+ event: EventName | eventNS,
+ listener: ListenerFn<EventName, Events>,
+ options?: boolean | OnOptions
+ ): this | Listener<EventName, Events>
+
+ many<EventName extends keyof Events>(
+ event: EventName | eventNS,
+ timesToListen: number,
+ listener: ListenerFn<EventName, Events>,
+ options?: boolean | OnOptions
+ ): this | Listener<EventName, Events>
+
+ prependMany<EventName extends keyof Events>(
+ event: EventName | eventNS,
+ timesToListen: number,
+ listener: ListenerFn<EventName, Events>,
+ options?: boolean | OnOptions
+ ): this | Listener<EventName, Events>
+
+ onAny<EventName extends keyof Events>(
+ listener: EventAndListener<EventName, Events>
+ ): this
+
+ prependAny<EventName extends keyof Events>(
+ listener: EventAndListener<EventName, Events>
+ ): this
+
+ offAny<EventName extends keyof Events>(
+ listener: ListenerFn<EventName, Events>
+ ): this
+
+ removeListener<EventName extends keyof Events>(
+ event: EventName | eventNS,
+ listener: ListenerFn<EventName, Events>
+ ): this
+
+ off<EventName extends keyof Events>(
+ event: EventName | eventNS,
+ listener: ListenerFn<EventName, Events>
+ ): this
+
+ removeAllListeners<EventName extends keyof Events>(
+ event?: EventName | eventNS
+ ): this
+
+ setMaxListeners(numberOfListeners: number): void
+
+ getMaxListeners(): number
+
+ eventNames<EventName extends keyof Events>(
+ nsAsArray?: boolean
+ ): (EventName | eventNS)[]
+
+ listenerCount<EventName extends keyof Events>(
+ event?: EventName | eventNS
+ ): number
+
+ listeners<EventName extends keyof Events>(
+ event?: EventName | eventNS
+ ): ListenerFn<EventName, Events>[]
+
+ listenersAny<EventName extends keyof Events>(): ListenerFn<
+ EventName,
+ Events
+ >[]
+
+ waitFor<EventName extends keyof Events>(
+ event: EventName | eventNS,
+ timeout?: number
+ ): CancelablePromise<ListenerFunctionParameters<EventName, Events>>
+ waitFor<EventName extends keyof Events>(
+ event: EventName | eventNS,
+ filter?: WaitForFilter<EventName, Events>
+ ): CancelablePromise<ListenerFunctionParameters<EventName, Events>>
+ waitFor<EventName extends keyof Events>(
+ event: EventName | eventNS,
+ options?: WaitForOptions<EventName, Events>
+ ): CancelablePromise<ListenerFunctionParameters<EventName, Events>>
+
+ listenTo<EventName extends keyof Events>(
+ target: GeneralEventEmitter<EventName, Events>,
+ events: EventName | eventNS,
+ options?: ListenToOptions<EventName, Events>
+ ): this
+ listenTo<EventName extends keyof Events>(
+ target: GeneralEventEmitter<EventName, Events>,
+ events: EventName[],
+ options?: ListenToOptions<EventName, Events>
+ ): this
+ listenTo<EventName extends keyof Events>(
+ target: GeneralEventEmitter<EventName, Events>,
+ events: Record<string | number | symbol, unknown>,
+ options?: ListenToOptions<EventName, Events>
+ ): this
+
+ stopListeningTo<EventName extends keyof Events>(
+ target?: GeneralEventEmitter<EventName, Events>,
+ event?: EventName | eventNS
+ ): boolean
+
+ hasListeners<EventName extends keyof Events>(event?: EventName): boolean
+
+ static once<
+ EventName extends keyof Events = event,
+ Events extends EventMap = EventMap
+ >(
+ emitter: EventEmitter2<Events>,
+ event: EventName | eventNS,
+ options?: OnceOptions
+ ): CancelablePromise<ListenerFunctionParameters<EventName, Events>>
+
+ static defaultMaxListeners: number
}
export default EventEmitter2;
\ No newline at end of file

View file

@ -3,6 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { EventMap } from 'eventemitter2';
export const eventModuleConfig = {
wildcard: false,
@ -18,3 +19,7 @@ export enum NoteEvent {
PERMISSION_CHANGE = 'note.permission_change' /** noteId: The id of the [@link Note], which permissions are changed. **/,
DELETION = 'note.deletion' /** noteId: The id of the [@link Note], which is being deleted. **/,
}
export interface NoteEventMap extends EventMap {
[NoteEvent.PERMISSION_CHANGE]: (noteId: number) => void;
}

View file

@ -17,7 +17,7 @@ import {
MaximumDocumentLengthExceededError,
NotInDBError,
} from '../errors/errors';
import { NoteEvent } from '../events';
import { NoteEvent, NoteEventMap } from '../events';
import { Group } from '../groups/group.entity';
import { GroupsService } from '../groups/groups.service';
import { HistoryEntry } from '../history/history-entry.entity';
@ -54,7 +54,7 @@ export class NotesService {
@Inject(forwardRef(() => AliasService)) private aliasService: AliasService,
private realtimeNoteService: RealtimeNoteService,
private realtimeNoteStore: RealtimeNoteStore,
private eventEmitter: EventEmitter2,
private eventEmitter: EventEmitter2<NoteEventMap>,
) {
this.logger.setContext(NotesService.name);
}

View file

@ -14,7 +14,7 @@ import {
} from '../config/guest_access.enum';
import noteConfiguration, { NoteConfig } from '../config/note.config';
import { PermissionsUpdateInconsistentError } from '../errors/errors';
import { NoteEvent } from '../events';
import { NoteEvent, NoteEventMap } from '../events';
import { Group } from '../groups/group.entity';
import { GroupsService } from '../groups/groups.service';
import { SpecialGroup } from '../groups/groups.special';
@ -36,7 +36,7 @@ export class PermissionsService {
private readonly logger: ConsoleLoggerService,
@Inject(noteConfiguration.KEY)
private noteConfig: NoteConfig,
private eventEmitter: EventEmitter2,
private eventEmitter: EventEmitter2<NoteEventMap>,
) {}
/**

View file

@ -8,7 +8,7 @@ import {
encodeMetadataUpdatedMessage,
} from '@hedgedoc/commons';
import { Logger } from '@nestjs/common';
import { EventEmitter2 } from 'eventemitter2';
import { EventEmitter2, EventMap } from 'eventemitter2';
import { Awareness } from 'y-protocols/awareness';
import { Note } from '../../notes/note.entity';
@ -16,10 +16,15 @@ import { WebsocketAwareness } from './websocket-awareness';
import { WebsocketConnection } from './websocket-connection';
import { WebsocketDoc } from './websocket-doc';
export interface MapType extends EventMap {
destroy: () => void;
beforeDestroy: () => void;
}
/**
* Represents a note currently being edited by a number of clients.
*/
export class RealtimeNote extends EventEmitter2 {
export class RealtimeNote extends EventEmitter2<MapType> {
protected logger: Logger;
private readonly websocketDoc: WebsocketDoc;
private readonly websocketAwareness: WebsocketAwareness;

View file

@ -7,13 +7,13 @@ import { EventEmitter2 } from 'eventemitter2';
import { Mock } from 'ts-mockery';
import { Note } from '../../../notes/note.entity';
import { RealtimeNote } from '../realtime-note';
import { MapType, RealtimeNote } from '../realtime-note';
import { WebsocketAwareness } from '../websocket-awareness';
import { WebsocketDoc } from '../websocket-doc';
import { mockAwareness } from './mock-awareness';
import { mockWebsocketDoc } from './mock-websocket-doc';
class MockRealtimeNote extends EventEmitter2 {
class MockRealtimeNote extends EventEmitter2<MapType> {
constructor(
private note: Note,
private doc: WebsocketDoc,

View file

@ -17,7 +17,7 @@ import type { AsyncState } from 'react-use/lib/useAsyncFn'
export const useLoadNoteFromServer = (): [AsyncState<boolean>, () => void] => {
const id = useSingleStringUrlParameter('noteId', undefined)
return useAsyncFn(async () => {
return useAsyncFn(async (): Promise<boolean> => {
if (id === undefined) {
throw new Error('Invalid id')
}

View file

@ -7,7 +7,8 @@ import type { TaskCheckedEventPayload } from '../../../../extensions/extra-integ
import { TaskListCheckboxAppExtension } from '../../../../extensions/extra-integrations/task-list/task-list-checkbox-app-extension'
import { WaitSpinner } from '../../../common/wait-spinner/wait-spinner'
import { eventEmitterContext } from '../../../markdown-renderer/hooks/use-extension-event-emitter'
import EventEmitter2 from 'eventemitter2'
import type { Listener } from 'eventemitter2'
import { EventEmitter2 } from 'eventemitter2'
import React, { Suspense, useEffect, useMemo } from 'react'
export interface CheatsheetLineProps {
@ -32,10 +33,13 @@ export const CheatsheetLine: React.FC<CheatsheetLineProps> = ({ markdown, onTask
const eventEmitter = useMemo(() => new EventEmitter2(), [])
useEffect(() => {
const handler = ({ checked }: TaskCheckedEventPayload) => onTaskCheckedChange(checked)
eventEmitter.on(TaskListCheckboxAppExtension.EVENT_NAME, handler)
const handler = eventEmitter.on(
TaskListCheckboxAppExtension.EVENT_NAME,
({ checked }: TaskCheckedEventPayload) => onTaskCheckedChange(checked),
{ objectify: true }
) as Listener
return () => {
eventEmitter.off(TaskListCheckboxAppExtension.EVENT_NAME, handler)
handler.off()
}
})

View file

@ -5,7 +5,7 @@
*/
import type { NodeProcessor } from '../../node-preprocessors/node-processor'
import type { ComponentReplacer } from '../../replace-components/component-replacer'
import type EventEmitter2 from 'eventemitter2'
import type { EventEmitter2 } from 'eventemitter2'
import type MarkdownIt from 'markdown-it'
/**

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import EventEmitter2 from 'eventemitter2'
import { EventEmitter2 } from 'eventemitter2'
import type { PropsWithChildren } from 'react'
import React, { createContext, useContext, useEffect, useMemo } from 'react'

View file

@ -14,7 +14,7 @@ import { useRendererReceiveHandler } from './window-post-message-communicator/ho
import type { BaseConfiguration } from './window-post-message-communicator/rendering-message'
import { CommunicationMessageType, RendererType } from './window-post-message-communicator/rendering-message'
import { countWords } from './word-counter'
import EventEmitter2 from 'eventemitter2'
import { EventEmitter2 } from 'eventemitter2'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
/**
@ -174,10 +174,10 @@ export const IframeMarkdownRenderer: React.FC = () => {
const extensionEventEmitter = useMemo(() => new EventEmitter2({ wildcard: true }), [])
useEffect(() => {
extensionEventEmitter.onAny((event, values) => {
extensionEventEmitter.onAny((event: string, values: unknown) => {
communicator.sendMessageToOtherSide({
type: CommunicationMessageType.EXTENSION_EVENT,
eventName: typeof event === 'object' ? event.join('.') : event,
eventName: event,
payload: values
})
})

View file

@ -5,7 +5,7 @@
*/
import type { Logger } from '../../../utils/logger'
import { Optional } from '@mrdrogdrog/optional'
import EventEmitter2 from 'eventemitter2'
import { EventEmitter2 } from 'eventemitter2'
/**
* Error that will be thrown if a message couldn't be sent.

View file

@ -5,7 +5,7 @@
*/
import type { Linter } from '../../components/editor-page/editor-pane/linter/linter'
import type { MarkdownRendererExtension } from '../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
import type EventEmitter2 from 'eventemitter2'
import type { EventEmitter2 } from 'eventemitter2'
import type React from 'react'
import { Fragment } from 'react'

View file

@ -6,7 +6,7 @@
import { AppExtension } from '../../base/app-extension'
import { SetCheckboxInEditor } from './set-checkbox-in-editor'
import { TaskListMarkdownExtension } from './task-list-markdown-extension'
import type EventEmitter2 from 'eventemitter2'
import type { EventEmitter2 } from 'eventemitter2'
import type React from 'react'
/**

View file

@ -28,7 +28,8 @@
"@codemirror/theme-one-dark": "6.1.0",
"@types/react": "18.0.27",
"y-protocols@^1.0.0": "patch:y-protocols@npm%3A1.0.5#./.yarn/patches/y-protocols-npm-1.0.5-af6f64b4df.patch",
"y-protocols@1.0.5": "patch:y-protocols@npm%3A1.0.5#./.yarn/patches/y-protocols-npm-1.0.5-af6f64b4df.patch"
"y-protocols@1.0.5": "patch:y-protocols@npm%3A1.0.5#./.yarn/patches/y-protocols-npm-1.0.5-af6f64b4df.patch",
"eventemitter2@6.4.9": "patch:eventemitter2@npm%3A6.4.9#./.yarn/patches/eventemitter2-npm-6.4.9-ba37798a18.patch"
},
"devDependencies": {
"turbo": "1.6.3"

View file

@ -9298,6 +9298,13 @@ __metadata:
languageName: node
linkType: hard
"eventemitter2@patch:eventemitter2@npm%3A6.4.9#./.yarn/patches/eventemitter2-npm-6.4.9-ba37798a18.patch::locator=hedgedoc%40workspace%3A.":
version: 6.4.9
resolution: "eventemitter2@patch:eventemitter2@npm%3A6.4.9#./.yarn/patches/eventemitter2-npm-6.4.9-ba37798a18.patch::version=6.4.9&hash=7bad23&locator=hedgedoc%40workspace%3A."
checksum: 058a7ea1566747b005835f0c1f2348aabd355c819c1d63d4d879a476503f51c5bde93f72796813dcdae066fe6e4535c6415b91c66acaadc53251b54a274c9263
languageName: node
linkType: hard
"eventemitter3@npm:^4.0.0, eventemitter3@npm:^4.0.4":
version: 4.0.7
resolution: "eventemitter3@npm:4.0.7"