import { type Logger, loggerDebug, type LogOptions } from "@foxxmd/logging"; import { EventEmitter } from "events"; import { createContainer } from "iti"; import { getConfigDir } from "./common/index.ts"; import { WildcardEmitter } from "./common/WildcardEmitter.ts"; import { generateBaseURL } from "./utils/NetworkUtils.ts"; import type { PassThrough } from "stream"; import type {CacheConfigOptions} from "./common/infrastructure/Atomic.ts"; import type { MusicBrainzSingletonMap } from './common/vendor/musicbrainz/MusicbrainzTypes.ts'; import { MSCache } from "./common/Cache.ts"; import TransformerManager from "./common/transforms/TransformerManager.ts"; import type {TransformerCommonConfig} from "../core/Atomic.ts"; import prom from 'prom-client'; import { CoverArtApiClient } from "./common/vendor/musicbrainz/CoverArtApiClient.ts"; import { version, stable } from "./version.ts"; import type {DbConcrete} from "./common/database/drizzle/drizzleUtils.ts"; import type { MSBackendEventMap } from "./common/infrastructure/MSBackendEventMap.ts"; import type { RockskySingletonMap } from "./common/vendor/rocksky/RockskyClientWrapped.ts"; import type { CovertArtSingletonMap } from "./common/vendor/musicbrainz/CovertArtApiPool.ts"; let root: ReturnType; export interface RootOptions { baseUrl?: string, port?: number logger: Logger disableWeb?: boolean loggerStream?: PassThrough loggingConfig?: LogOptions cache?: CacheConfigOptions | MSCache | (() => MSCache) mbMap?: MusicBrainzSingletonMap | (() => MusicBrainzSingletonMap) rsMap?: RockskySingletonMap | (() => RockskySingletonMap) caMap?: CovertArtSingletonMap | (() => CovertArtSingletonMap) transformers?: TransformerCommonConfig[] db?: DbConcrete | (() => Promise) } const discovered = new prom.Counter({ name: 'multiscrobbler_source_discovered', help: 'Number of discovered plays for a Source', labelNames: ['name', 'type'] }); const sourceDead = new prom.Gauge({ name: 'multiscrobbler_source_dead', help: 'Number of dead letter plays for a Source', labelNames: ['name', 'type'] }); const queuedGauge = new prom.Gauge({ name: 'multiscrobbler_client_queued', help: 'Number of queued plays for a Client', labelNames: ['name', 'type'] }); const queuedSourceGauge = new prom.Gauge({ name: 'multiscrobbler_source_queued', help: 'Number of queued plays for a Source', labelNames: ['name', 'type'] }); const deadLetterGauge = new prom.Gauge({ name: 'multiscrobbler_client_deadletter', help: 'Number of deadletter plays for a Client', labelNames: ['name', 'type'] }); const scrobbledCounter = new prom.Counter({ name: 'multiscrobbler_client_scrobbled', help: 'Number of discovered plays for a Source', labelNames: ['name', 'type'] }); const createRoot = (options: RootOptions = {logger: loggerDebug}) => { const { port = 9078, baseUrl = process.env.BASE_URL, disableWeb: dw, loggerStream, loggingConfig, logger, cache, mbMap, rsMap, caMap, db, transformers = [] } = options || {}; let disableWeb = dw; if(disableWeb === undefined) { disableWeb = process.env.DISABLE_WEB === 'true'; } let cacheFunc: () => MSCache; let maybeSingletonCache: MSCache; if(cache instanceof MSCache) { maybeSingletonCache = cache; } else if(typeof cache === 'function') { cacheFunc = cache; } else { maybeSingletonCache = new MSCache(logger, cache); } let mbFunc: () => MusicBrainzSingletonMap; let maybeSingletonMb: MusicBrainzSingletonMap; if(typeof mbMap === 'function') { mbFunc = mbMap; } else if(maybeSingletonMb !== undefined) { maybeSingletonMb = mbMap; } else { maybeSingletonMb = new Map(); } let rsFunc: () => RockskySingletonMap; let maybeSingletonRs: RockskySingletonMap; if(typeof rsMap === 'function') { rsFunc = rsMap; } else if(maybeSingletonRs !== undefined) { maybeSingletonRs = rsMap; } else { maybeSingletonRs = new Map(); } let caFunc: () => CovertArtSingletonMap; let maybeSingletonCa: CovertArtSingletonMap; if(typeof caMap === 'function') { caFunc = caMap; } else if(maybeSingletonCa !== undefined) { maybeSingletonCa = caMap; } else { maybeSingletonCa = new Map(); } let dbFunc: () => Promise; if(typeof db === 'function') { dbFunc = db; } else { dbFunc = async () => db; } const cEmitter = new WildcardEmitter(); // do nothing, just catch cEmitter.on('error', (e) => { logger.warn(new Error('Client emitter threw an error', {cause: e})); }); const sEmitter = new WildcardEmitter(); sEmitter.on('error', (e) => { logger.warn(new Error('Source emitter threw an error', {cause: e})); }); const transformerManager = new TransformerManager(logger, maybeSingletonCache !== undefined ? maybeSingletonCache : cacheFunc()); for(const c of transformers) { transformerManager.addTransformerConfig(c); } if(transformers.length === 0) { logger.debug('No user-supplied transformer configs were found.'); } const cacheApi = (maybeSingletonCache !== undefined ? maybeSingletonCache : cacheFunc()).cacheApi; const coverArtApi = new CoverArtApiClient('', {}, {logger, cache: cacheApi}); const portVal: number | string = process.env.PORT ?? port; const versionParts: string[] = []; if(stable === version) { versionParts.push(stable); } else { if(stable !== 'unknown') { versionParts.push(stable); } versionParts.push(version); } return createContainer().add({ version: versionParts.join('-'), configDir: getConfigDir(), isProd: process.env.NODE_ENV !== undefined && (process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'prod'), // @ts-ignore port: (Number.isInteger(portVal) ? portVal : Number.parseInt(portVal)) as number, disableWeb, clientEmitter: () => cEmitter, sourceEmitter: () => sEmitter, notifierEmitter: () => new EventEmitter(), loggerStream, loggingConfig, sourceMetics: { discovered: discovered, queued: queuedSourceGauge, deadLetter: sourceDead //issues: sourceIssues }, clientMetrics: { queued: queuedGauge, scrobbled: scrobbledCounter, //issues: issuesClientGauge, deadLetter: deadLetterGauge }, logger: logger, transformerManager, cache: () => maybeSingletonCache !== undefined ? () => maybeSingletonCache : cacheFunc, mbMap: () => maybeSingletonMb !== undefined ? () => maybeSingletonMb : mbFunc, rsMap: () => maybeSingletonRs !== undefined ? () => maybeSingletonRs : rsFunc, caMap: () => maybeSingletonCa !== undefined ? () => maybeSingletonCa : caFunc, coverArtApi, db: () => dbFunc }).add((items) => { const localUrl = generateBaseURL(baseUrl, items.port) return { localUrl, hasDefinedBaseUrl: baseUrl !== undefined, isSubPath: localUrl.pathname !== '/' && localUrl.pathname.length > 0 } }); } export const getRoot = (options?: RootOptions) => { if(root === undefined) { root = createRoot(options); } return root; } export default createRoot;