From ac4a6db133c76708bf6e45e8beb6f895c1c8e634 Mon Sep 17 00:00:00 2001 From: brokencube Date: Sat, 2 May 2026 18:07:57 +0100 Subject: [PATCH] Clean up service manager - move all services to manager --- src/core/service.ts | 40 ++++++++++++++++++++++-------- src/interfaces/ServiceInterface.ts | 6 ----- src/services/CliService.ts | 34 ++++++++++++++----------- src/services/DatabaseService.ts | 6 ++--- src/services/FFMpegService.ts | 10 +++++--- src/tasks/ScanFoldersTask.ts | 7 +++--- 6 files changed, 61 insertions(+), 42 deletions(-) delete mode 100644 src/interfaces/ServiceInterface.ts diff --git a/src/core/service.ts b/src/core/service.ts index 181cad3..294835c 100644 --- a/src/core/service.ts +++ b/src/core/service.ts @@ -1,18 +1,18 @@ -import ServiceInterface from "../interfaces/ServiceInterface"; - type Constructor = new (...args:any[]) => any; - const serviceNameSymbol = Symbol("symbol.serviceName"); -export abstract class BaseService implements ServiceInterface { +export abstract class BaseService { private startPromise: Promise | null = null; async start(): Promise { if (!this.startPromise) { - await ServiceManager.get().startDependencies( - // @ts-ignore - this.constructor[Symbol.metadata][serviceNameSymbol] - ); + // @ts-ignore Retrieve service name from subclass metadata. (set by @service decorator) + const serviceName = this.constructor[Symbol.metadata]?.[serviceNameSymbol]; + + // If the service is properly labeled, use it's serviceName to ensure all dependent services have been started first. + if (serviceName) await ServiceManager.get().startDependencies(serviceName); + + // Start the service by running the (subclass provided) init() function. this.startPromise = new Promise(async (resolve) => { resolve(await this.init()); }) @@ -20,16 +20,24 @@ export abstract class BaseService implements ServiceInterface { return this.startPromise; } - abstract init(): Promise; + /** + * Abstract method to provide functionality to "start" the service + */ + protected abstract init(): Promise; async stop(): Promise { + // Make sure we are not in the middle of starting the service await this.startPromise; + + // Call (subclass provided) destroy method await this.destroy(); this.startPromise = null; - return; } - abstract destroy(): Promise; + /** + * Abstract method to provide functionality to "stop" the service + */ + protected abstract destroy(): Promise; } type ServiceContainer = { @@ -80,6 +88,9 @@ export class ServiceManager { return this; } + /** + * Start all decorated (@service) services that derive from the BaseService class + */ async start() { // Start all constructed services for (const service of this.services.values()) { @@ -140,6 +151,13 @@ export class ServiceManager { return service.service as T; } + + replaceService(name: string, service: any) { + if (!this.services.has(name)) { + const serviceContainer = this.services.get(name) as ServiceContainer; + serviceContainer.service = service; + } + } } export function service(propertyName: string) { diff --git a/src/interfaces/ServiceInterface.ts b/src/interfaces/ServiceInterface.ts deleted file mode 100644 index f1f0ed7..0000000 --- a/src/interfaces/ServiceInterface.ts +++ /dev/null @@ -1,6 +0,0 @@ -export default interface ServiceInterface { - start(): Promise; - init(): Promise; - stop(): Promise; - destroy(): Promise; -} \ No newline at end of file diff --git a/src/services/CliService.ts b/src/services/CliService.ts index 2bc8a94..1268432 100644 --- a/src/services/CliService.ts +++ b/src/services/CliService.ts @@ -1,22 +1,26 @@ import { spawn } from 'node:child_process'; import { once } from 'node:events'; +import {service} from "../core/service"; -export async function runCommand(command: string, args: string[]) { - const cmd = spawn(command, args); - const stdout = [] as string[]; - const stderr = [] as string[]; +@service('cliService') +export class CliService { + async runCommand(command: string, args: string[]) { + const cmd = spawn(command, args); + const stdout = [] as string[]; + const stderr = [] as string[]; - cmd.stdout?.on('data', (data: Buffer) => { - stdout.push(data.toString('utf-8')); - }); + cmd.stdout?.on('data', (data: Buffer) => { + stdout.push(data.toString('utf-8')); + }); - cmd.stderr?.on('data', (data: Buffer) => { - stderr.push(data.toString('utf-8')); - }); + cmd.stderr?.on('data', (data: Buffer) => { + stderr.push(data.toString('utf-8')); + }); - const [code] = await once(cmd, 'close'); - return { - stdout: stdout.join(''), - stderr: stderr.join(''), - }; + const [code] = await once(cmd, 'close'); + return { + stdout: stdout.join(''), + stderr: stderr.join(''), + }; + } } diff --git a/src/services/DatabaseService.ts b/src/services/DatabaseService.ts index 3906687..5e79b9f 100644 --- a/src/services/DatabaseService.ts +++ b/src/services/DatabaseService.ts @@ -11,21 +11,21 @@ export default class DatabaseService extends BaseService { protected db: Database.Database; @inject('configService') protected accessor config!: ConfigService; + protected migrations: Migration[]; constructor() { super(); this.db = new Database('database/core.db'); this.db.pragma('journal_mode = WAL'); + this.migrations = this.config.migrations; } async init() { - const migrations = this.config.migrations; - this.assertMigrationTable(); // Turn array of migrations into dictionary const migrationDict = {} as {[key: string]: Migration}; - for (const migration of migrations) { + for (const migration of this.migrations) { migrationDict[migration.name] = migration; } diff --git a/src/services/FFMpegService.ts b/src/services/FFMpegService.ts index 234123b..fbb3db2 100644 --- a/src/services/FFMpegService.ts +++ b/src/services/FFMpegService.ts @@ -1,10 +1,12 @@ -import {runCommand} from "./CliService"; -import {service} from "../core/service"; +import {inject, service} from "../core/service"; +import {CliService} from "./CliService"; @service('ffmpegService') export default class FFMpegService { - static async checkFile(filename: string) { - const file = await runCommand('ffprobe', [ + @inject('cliService') protected accessor cliService!: CliService; + + async checkFile(filename: string) { + const file = await this.cliService.runCommand('ffprobe', [ '-v', 'quiet', '-print_format', diff --git a/src/tasks/ScanFoldersTask.ts b/src/tasks/ScanFoldersTask.ts index 784fe01..67027b3 100644 --- a/src/tasks/ScanFoldersTask.ts +++ b/src/tasks/ScanFoldersTask.ts @@ -4,12 +4,13 @@ import {Dirent} from "node:fs"; import {fileTypeFromFile} from "file-type"; import FFMpegService from "../services/FFMpegService"; import TaskInterface from "../interfaces/TaskInterface"; +import {inject} from "../core/service"; export default class ScanFoldersTask implements TaskInterface { private hasRun: boolean = false; + @inject('ffmpegService') protected accessor ffmpegService!: FFMpegService; - constructor(protected mount: string) { - } + constructor(protected mount: string) {} shouldRun(): boolean { return !this.hasRun; @@ -37,7 +38,7 @@ export default class ScanFoldersTask implements TaskInterface { console.log('file', pathname); const type = await fileTypeFromFile(pathname); if (type === undefined || type.mime.startsWith('audio')) { - console.log(await FFMpegService.checkFile(pathname)); + console.log(await this.ffmpegService.checkFile(pathname)); } } } \ No newline at end of file