Skip to content

Commit ee68c4c

Browse files
committed
refactor: Move FolderWatcher and ManagedChildProcess to use TypedEmitter
1 parent 2458831 commit ee68c4c

4 files changed

Lines changed: 73 additions & 54 deletions

File tree

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/back/ManagedChildProcess.ts

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,15 @@ import { INamedBackProcessInfo } from '@shared/interfaces';
22
import * as Coerce from '@shared/utils/Coerce';
33
import { ChildProcess, spawn } from 'child_process';
44
import { EventEmitter } from 'events';
5-
import { IBackProcessInfo, ILogPreEntry } from 'flashpoint-launcher';
5+
import { IBackProcessInfo, ManagedChildProcessEvents, ProcessState } from 'flashpoint-launcher';
66
import * as readline from 'readline';
77
import * as kill from 'tree-kill';
8+
import { TypedEmitter } from 'typed-emitter';
89
import { onServiceChange } from './util/events';
910
import { Disposable } from './util/lifecycle';
10-
import { ProcessState } from 'flashpoint-launcher';
1111

1212
const { str } = Coerce;
1313

14-
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
15-
export interface ManagedChildProcess {
16-
/**
17-
* Fires when any background service prints to std{out,err}. Every line is
18-
* prefixed with the name of the process and the output is guaranteed to end
19-
* with a new line.
20-
*/
21-
on(event: 'output', handler: (output: ILogPreEntry) => void): this;
22-
emit(event: 'output', output: ILogPreEntry): boolean;
23-
/** Fires whenever the status of a process changes. */
24-
on(event: 'change', listener: (newState: ProcessState) => void): this;
25-
emit(event: 'change', newState: ProcessState): boolean;
26-
/** Fires whenever the process exits */
27-
on(event: 'exit', listener: (code: number | null, signal: string | null) => void): this;
28-
emit(event: 'exit', code: number | null, signal: string | null): boolean;
29-
}
30-
3114
export type ProcessOpts = {
3215
detached?: boolean;
3316
autoRestart?: boolean;
@@ -40,8 +23,7 @@ export type ProcessOpts = {
4023
const MAX_RESTARTS = 5;
4124

4225
/** Manages a single process. Wrapper around node's ChildProcess. */
43-
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
44-
export class ManagedChildProcess extends EventEmitter {
26+
export class ManagedChildProcess extends (EventEmitter as new () => TypedEmitter<ManagedChildProcessEvents>) {
4527
// @TODO Add timeouts for restarting and killing the process (it should give up after some time, like 10 seconds) maybe?
4628

4729
public id: string;

src/back/util/FolderWatcher.ts

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { debounce } from '@shared/utils/debounce';
22
import * as chokidar from 'chokidar';
33
import * as fs from 'node:fs';
44
import * as path from 'node:path';
5-
import { WrappedEventEmitter } from './WrappedEventEmitter';
5+
import { EventEmitter } from 'node:stream';
6+
import { TypedEmitter } from 'typed-emitter';
67

78
type IMap<K extends string | number, V> = { [key in K]: V; };
89

@@ -11,31 +12,20 @@ export type FolderWatcherOptions = {
1112
changeDebounce?: number;
1213
}
1314

15+
type FolderWatcherEvents = {
16+
ready: () => void;
17+
change: (filename: string, offsetPath: string) => void;
18+
add: (filename: string, offsetPath: string) => void;
19+
remove: (filename: string, stats: fs.Stats, offsetPath: string) => void;
20+
error: (error: Error) => void;
21+
}
22+
1423
/**
1524
* Watches a folder and its child files/folders for changes using chokidar.
1625
* Recursive watching is optional.
1726
* An instance of this can only be used to watch one folder once, you can not watch after aborting.
1827
*/
19-
export class FolderWatcher extends WrappedEventEmitter {
20-
// Override the base class methods with specific overloads
21-
on(event: 'ready', listener: () => void): this;
22-
on(event: 'change', listener: (filename: string, offsetPath: string) => void): this;
23-
on(event: 'add', listener: (filename: string, offsetPath: string) => void): this;
24-
on(event: 'remove', listener: (filename: string, stats: fs.Stats, offsetPath: string) => void): this;
25-
on(event: 'error', listener: (error: Error) => void): this;
26-
on(event: string, listener: (...args: any[]) => void): this {
27-
return super.on(event, listener);
28-
}
29-
30-
once(event: 'ready', listener: () => void): this;
31-
once(event: 'change', listener: (filename: string, offsetPath: string) => void): this;
32-
once(event: 'add', listener: (filename: string, offsetPath: string) => void): this;
33-
once(event: 'remove', listener: (filename: string, stats: fs.Stats, offsetPath: string) => void): this;
34-
once(event: 'error', listener: (error: Error) => void): this;
35-
once(event: string, listener: (...args: any[]) => void): this {
36-
return super.once(event, listener);
37-
}
38-
28+
export class FolderWatcher extends (EventEmitter as new () => TypedEmitter<FolderWatcherEvents>) {
3929
/** Chokidar watcher instance. */
4030
protected _watcher: chokidar.FSWatcher | undefined;
4131
/** Map of child files/folders of the watched folder (["filename"] = "file stats"). */

typings/flashpoint-launcher.d.ts

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
21
// Type definitions for non-npm package flashpoint-launcher 12.2
32
// Project: Flashpoint Launcher https://github.com/FlashpointProject/launcher
43
// Definitions by: Colin Berry <https://github.com/colin969>
@@ -24,6 +23,7 @@
2423
declare module 'flashpoint-launcher' {
2524
import { FlashpointArchive, GameSearch } from '@fparchive/flashpoint-archive';
2625
import { Readable } from 'stream';
26+
import { TypedEmitter } from 'typed-emitter';
2727

2828
/** Version of the Flashpoint Launcher */
2929
const version: string;
@@ -1398,16 +1398,13 @@ declare module 'flashpoint-launcher' {
13981398
state: DownloaderStatus;
13991399
}
14001400

1401-
interface ManagedChildProcess {
1402-
/** Fires whenever the status of a process changes. */
1403-
on(event: 'change', listener: (newState: ProcessState) => void): this;
1404-
emit(event: 'change', newState: ProcessState): boolean;
1405-
/** Fires whenever the process exits */
1406-
on(event: 'exit', listener: (code: number | null, signal: string | null) => void): this;
1407-
emit(event: 'exit', code: number | null, signal: string | null): boolean;
1401+
type ManagedChildProcessEvents = {
1402+
output: (output: ILogPreEntry) => void;
1403+
change: (newState: ProcessState) => void;
1404+
exit: (code: number | null, signal: string | null) => void;
14081405
}
14091406

1410-
class ManagedChildProcess {
1407+
class ManagedChildProcess extends TypedEmitter<ManagedChildProcessEvents> {
14111408
/** ID of the process */
14121409
id: string;
14131410
/** Info this process was created with */
@@ -3639,3 +3636,53 @@ declare module 'flashpoint-launcher-renderer-ext/actions/main' {
36393636
const removeGameSidebarComponent: ActionCreatorWithPayload<DisplaySettingsGameSidebarAction>;
36403637
const setExtConfigValue: ActionCreatorWithPayload<ExtConfigValueAction>;
36413638
}
3639+
3640+
/**
3641+
* Typings lifted from typed-emitter NPM
3642+
* https://github.com/andywer/typed-emitter
3643+
*
3644+
* MIT License, Copyright (c) 2018 Andy Wermke
3645+
**/
3646+
declare module 'typed-emitter' {
3647+
export type EventMap = {
3648+
[key: string]: (...args: any[]) => void
3649+
}
3650+
3651+
/**
3652+
* Type-safe event emitter.
3653+
*
3654+
* Use it like this:
3655+
*
3656+
* ```typescript
3657+
* type MyEvents = {
3658+
* error: (error: Error) => void;
3659+
* message: (from: string, content: string) => void;
3660+
* }
3661+
*
3662+
* const myEmitter = new EventEmitter() as TypedEmitter<MyEvents>;
3663+
*
3664+
* myEmitter.emit("error", "x") // <- Will catch this type error;
3665+
* ```
3666+
*/
3667+
export interface TypedEmitter<Events extends EventMap> {
3668+
addListener<E extends keyof Events> (event: E, listener: Events[E]): this
3669+
on<E extends keyof Events> (event: E, listener: Events[E]): this
3670+
once<E extends keyof Events> (event: E, listener: Events[E]): this
3671+
prependListener<E extends keyof Events> (event: E, listener: Events[E]): this
3672+
prependOnceListener<E extends keyof Events> (event: E, listener: Events[E]): this
3673+
3674+
off<E extends keyof Events>(event: E, listener: Events[E]): this
3675+
removeAllListeners<E extends keyof Events> (event?: E): this
3676+
removeListener<E extends keyof Events> (event: E, listener: Events[E]): this
3677+
3678+
emit<E extends keyof Events> (event: E, ...args: Parameters<Events[E]>): boolean
3679+
// The sloppy `eventNames()` return type is to mitigate type incompatibilities - see #5
3680+
eventNames (): (keyof Events | string | symbol)[]
3681+
rawListeners<E extends keyof Events> (event: E): Events[E][]
3682+
listeners<E extends keyof Events> (event: E): Events[E][]
3683+
listenerCount<E extends keyof Events> (event: E): number
3684+
3685+
getMaxListeners (): number
3686+
setMaxListeners (maxListeners: number): this
3687+
}
3688+
}

0 commit comments

Comments
 (0)