184 lines
4.1 KiB
JavaScript
Executable File

import chokidar from "chokidar"
import path from "path"
import { minimatch } from "minimatch"
import spawnService from "../utils/spawnService"
import getIgnoredFiles from "../utils/getIgnoredFiles"
/**
* Service class - Represents a single service instance
* Manages the lifecycle, communication, and monitoring of a service
*/
export default class Service {
/**
* @param {object} config - Service configuration
* @param {string} config.id - Service identifier
* @param {string} config.version - Service version
* @param {string} config.path - Path to service entry file
* @param {string} config.cwd - Current working directory
* @param {boolean} config.isProduction - Whether running in production mode
* @param {string} config.internalIp - Internal IP address
* @param {object} handlers - Event handlers
* @param {Function} handlers.onReady - Called when service is ready
* @param {Function} handlers.onIPCData - Called when IPC data is received
* @param {Function} handlers.onServiceExit - Called when service exits
*/
constructor(config, handlers) {
this.id = config.id
this.version = config.version
this.path = config.path
this.cwd = config.cwd
this.isProduction = config.isProduction
this.internalIp = config.internalIp
this.instance = null
this.fileWatcher = null
this.handlers = handlers
}
/**
* Initialize the service and start its process
*/
async initialize() {
await this.startProcess()
if (!this.isProduction) {
await this.setupFileWatcher()
}
}
/**
* Start the service process
*/
async startProcess() {
console.log(`🔰 [${this.id}] Starting service...`)
this.instance = await spawnService({
id: this.id,
service: this,
path: this.path,
cwd: this.cwd,
onClose: this.handleClose.bind(this),
onError: this.handleError.bind(this),
onIPCData: this.handleIPCData.bind(this),
})
// if debug flag is enabled, attach logs on start
this.instance.logs.attach()
return this.instance
}
/**
* Set up file watcher for development hot-reload
*/
async setupFileWatcher() {
const ignored = [
...(await getIgnoredFiles(this.cwd)),
"**/.cache/**",
"**/node_modules/**",
"**/dist/**",
"**/build/**",
]
this.fileWatcher = chokidar.watch(this.cwd, {
ignored: (path) =>
ignored.some((pattern) => minimatch(path, pattern)),
persistent: true,
ignoreInitial: true,
})
this.fileWatcher.on("all", (event, filePath) => {
console.log(`[${this.id}] File changed: ${event} ${filePath}`)
this.reload()
})
}
/**
* Handle IPC data from the service
* @param {string} id - Service ID
* @param {object} data - IPC data
*/
handleIPCData(id, data) {
if (this.handlers.onIPCData) {
this.handlers.onIPCData(this, data)
}
}
/**
* Handle service closure
* @param {string} id - Service ID
* @param {number} code - Exit code
* @param {Error} error - Error if any
*/
async handleClose(id, code, error) {
if (this.handlers.onServiceExit) {
await this.handlers.onServiceExit(this, code, error)
}
// In production, we might want to restart the service
if (this.isProduction && code !== 0) {
console.error(`[${this.id}] Service crashed, restarting...`)
await new Promise((resolve) => setTimeout(resolve, 1000))
await this.reload()
}
}
/**
* Handle service errors
* @param {string} id - Service ID
* @param {Error} error - Error object
*/
handleError(id, error) {
console.error(`[${this.id}] Error:`, error)
}
/**
* Reload the service
*/
async reload() {
console.log(`[${this.id}] Reloading service...`)
// Kill the current process if is running
if (this.instance.exitCode === null) {
await this.instance.kill()
}
// Start a new process
await this.startProcess()
return true
}
/**
* Stop the service
*/
stop() {
console.log(`[${this.id}] Stopping service...`)
this.instance.kill()
if (this.fileWatcher) {
this.fileWatcher.close()
}
}
/**
* Attach to service standard output
*/
attachStd() {
if (this.instance) {
this.instance.logs.attach()
}
}
/**
* Detach from service standard output
*/
detachStd() {
if (this.instance) {
this.instance.logs.detach()
}
}
}