Clean up service manager - move all services to manager

This commit is contained in:
2026-05-02 18:07:57 +01:00
parent 532e7eac81
commit ac4a6db133
6 changed files with 61 additions and 42 deletions
+29 -11
View File
@@ -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<void> | null = null;
async start(): Promise<void> {
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<void>(async (resolve) => {
resolve(await this.init());
})
@@ -20,16 +20,24 @@ export abstract class BaseService implements ServiceInterface {
return this.startPromise;
}
abstract init(): Promise<void>;
/**
* Abstract method to provide functionality to "start" the service
*/
protected abstract init(): Promise<void>;
async stop(): Promise<void> {
// 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<void>;
/**
* Abstract method to provide functionality to "stop" the service
*/
protected abstract destroy(): Promise<void>;
}
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) {
-6
View File
@@ -1,6 +0,0 @@
export default interface ServiceInterface {
start(): Promise<void>;
init(): Promise<void>;
stop(): Promise<void>;
destroy(): Promise<void>;
}
+19 -15
View File
@@ -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(''),
};
}
}
+3 -3
View File
@@ -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;
}
+6 -4
View File
@@ -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',
+4 -3
View File
@@ -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));
}
}
}