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; type Constructor = new (...args:any[]) => any;
const serviceNameSymbol = Symbol("symbol.serviceName"); const serviceNameSymbol = Symbol("symbol.serviceName");
export abstract class BaseService implements ServiceInterface { export abstract class BaseService {
private startPromise: Promise<void> | null = null; private startPromise: Promise<void> | null = null;
async start(): Promise<void> { async start(): Promise<void> {
if (!this.startPromise) { if (!this.startPromise) {
await ServiceManager.get().startDependencies( // @ts-ignore Retrieve service name from subclass metadata. (set by @service decorator)
// @ts-ignore const serviceName = this.constructor[Symbol.metadata]?.[serviceNameSymbol];
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) => { this.startPromise = new Promise<void>(async (resolve) => {
resolve(await this.init()); resolve(await this.init());
}) })
@@ -20,16 +20,24 @@ export abstract class BaseService implements ServiceInterface {
return this.startPromise; 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> { async stop(): Promise<void> {
// Make sure we are not in the middle of starting the service
await this.startPromise; await this.startPromise;
// Call (subclass provided) destroy method
await this.destroy(); await this.destroy();
this.startPromise = null; this.startPromise = null;
return;
} }
abstract destroy(): Promise<void>; /**
* Abstract method to provide functionality to "stop" the service
*/
protected abstract destroy(): Promise<void>;
} }
type ServiceContainer = { type ServiceContainer = {
@@ -80,6 +88,9 @@ export class ServiceManager {
return this; return this;
} }
/**
* Start all decorated (@service) services that derive from the BaseService class
*/
async start() { async start() {
// Start all constructed services // Start all constructed services
for (const service of this.services.values()) { for (const service of this.services.values()) {
@@ -140,6 +151,13 @@ export class ServiceManager {
return service.service as T; 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) { 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 { spawn } from 'node:child_process';
import { once } from 'node:events'; import { once } from 'node:events';
import {service} from "../core/service";
export async function runCommand(command: string, args: string[]) { @service('cliService')
const cmd = spawn(command, args); export class CliService {
const stdout = [] as string[]; async runCommand(command: string, args: string[]) {
const stderr = [] as string[]; const cmd = spawn(command, args);
const stdout = [] as string[];
const stderr = [] as string[];
cmd.stdout?.on('data', (data: Buffer) => { cmd.stdout?.on('data', (data: Buffer) => {
stdout.push(data.toString('utf-8')); stdout.push(data.toString('utf-8'));
}); });
cmd.stderr?.on('data', (data: Buffer) => { cmd.stderr?.on('data', (data: Buffer) => {
stderr.push(data.toString('utf-8')); stderr.push(data.toString('utf-8'));
}); });
const [code] = await once(cmd, 'close'); const [code] = await once(cmd, 'close');
return { return {
stdout: stdout.join(''), stdout: stdout.join(''),
stderr: stderr.join(''), stderr: stderr.join(''),
}; };
}
} }
+3 -3
View File
@@ -11,21 +11,21 @@ export default class DatabaseService extends BaseService {
protected db: Database.Database; protected db: Database.Database;
@inject('configService') protected accessor config!: ConfigService; @inject('configService') protected accessor config!: ConfigService;
protected migrations: Migration[];
constructor() { constructor() {
super(); super();
this.db = new Database('database/core.db'); this.db = new Database('database/core.db');
this.db.pragma('journal_mode = WAL'); this.db.pragma('journal_mode = WAL');
this.migrations = this.config.migrations;
} }
async init() { async init() {
const migrations = this.config.migrations;
this.assertMigrationTable(); this.assertMigrationTable();
// Turn array of migrations into dictionary // Turn array of migrations into dictionary
const migrationDict = {} as {[key: string]: Migration}; const migrationDict = {} as {[key: string]: Migration};
for (const migration of migrations) { for (const migration of this.migrations) {
migrationDict[migration.name] = migration; migrationDict[migration.name] = migration;
} }
+6 -4
View File
@@ -1,10 +1,12 @@
import {runCommand} from "./CliService"; import {inject, service} from "../core/service";
import {service} from "../core/service"; import {CliService} from "./CliService";
@service('ffmpegService') @service('ffmpegService')
export default class FFMpegService { export default class FFMpegService {
static async checkFile(filename: string) { @inject('cliService') protected accessor cliService!: CliService;
const file = await runCommand('ffprobe', [
async checkFile(filename: string) {
const file = await this.cliService.runCommand('ffprobe', [
'-v', '-v',
'quiet', 'quiet',
'-print_format', '-print_format',
+4 -3
View File
@@ -4,12 +4,13 @@ import {Dirent} from "node:fs";
import {fileTypeFromFile} from "file-type"; import {fileTypeFromFile} from "file-type";
import FFMpegService from "../services/FFMpegService"; import FFMpegService from "../services/FFMpegService";
import TaskInterface from "../interfaces/TaskInterface"; import TaskInterface from "../interfaces/TaskInterface";
import {inject} from "../core/service";
export default class ScanFoldersTask implements TaskInterface { export default class ScanFoldersTask implements TaskInterface {
private hasRun: boolean = false; private hasRun: boolean = false;
@inject('ffmpegService') protected accessor ffmpegService!: FFMpegService;
constructor(protected mount: string) { constructor(protected mount: string) {}
}
shouldRun(): boolean { shouldRun(): boolean {
return !this.hasRun; return !this.hasRun;
@@ -37,7 +38,7 @@ export default class ScanFoldersTask implements TaskInterface {
console.log('file', pathname); console.log('file', pathname);
const type = await fileTypeFromFile(pathname); const type = await fileTypeFromFile(pathname);
if (type === undefined || type.mime.startsWith('audio')) { if (type === undefined || type.mime.startsWith('audio')) {
console.log(await FFMpegService.checkFile(pathname)); console.log(await this.ffmpegService.checkFile(pathname));
} }
} }
} }