Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PoC] Components #4278

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions src/backend/components/component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import * as Registry from './registry'
import type {
ComponentEvents,
ComponentProducers,
ComponentExclusiveProducers
} from './registry/types'

abstract class Component {
public abstract readonly name: string

public abstract init(): Promise<void>

protected async addEventListener<T extends keyof ComponentEvents>(
name: T,
callback: ComponentEvents[T]
): Promise<void> {
return Registry.addEventListener(this, name, callback)
}

protected async addProducer<T extends keyof ComponentProducers>(
name: T,
producer: ComponentProducers[T]
): Promise<void> {
return Registry.addProducer(this, name, producer)
}

protected async addExclusiveProducer<
T extends keyof ComponentExclusiveProducers
>(name: T, producer: ComponentExclusiveProducers[T]): Promise<void> {
return Registry.addExclusiveProducer(this, name, producer)
}

protected async invokeEvent<T extends keyof ComponentEvents>(
name: T,
...args: Parameters<ComponentEvents[T]>
): Promise<void> {
return Registry.invokeEvent(this, name, ...args)
}

protected async invokeProducers<T extends keyof ComponentProducers>(
name: T,
...args: Parameters<ComponentProducers[T]>
): Promise<ReturnType<ComponentProducers[T]>[]> {
return Registry.invokeProducers(this, name, ...args)
}

protected async invokeExclusiveProducer<
T extends keyof ComponentExclusiveProducers
>(
name: T,
...args: Parameters<ComponentExclusiveProducers[T]>
): Promise<ReturnType<ComponentExclusiveProducers[T]>> {
return Registry.invokeExclusiveProducer(this, name, ...args)
}
}

export { Component }
23 changes: 23 additions & 0 deletions src/backend/components/core_components/LogFile/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { join } from 'path'

import { Component } from '../../component'
import { logBasePath } from '../../registry/paths'

import LogFile from './logfile'

declare module 'backend/components/registry/types' {
interface ComponentExclusiveProducers {
'logFile:getLogFile': (name: string) => LogFile
}
}

export default class LogFileComponent extends Component {
name = 'LogFile'

async init() {
await this.addExclusiveProducer('logFile:getLogFile', (name) => {
const file_path = join(logBasePath, name + '.log')
return new LogFile(file_path)
})
}
}
17 changes: 17 additions & 0 deletions src/backend/components/core_components/LogFile/logfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { appendFile, writeFile } from 'fs/promises'

export default class LogFile {
public readonly file_path: string

public constructor(file_path: string) {
this.file_path = file_path
}

public async logMessage(message: string): Promise<void> {
await appendFile(this.file_path, message.trimEnd() + '\n')
}

public async clear(): Promise<void> {
await writeFile(this.file_path, '')
}
}
35 changes: 35 additions & 0 deletions src/backend/components/core_components/LogFileWriter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Component } from 'backend/components/component'
import LogFile from '../LogFile/logfile'

declare module 'backend/components/registry/types' {
interface ComponentExclusiveProducers {
'logFileWriter:getAndPrepareLogFile': (name: string) => Promise<LogFile>
}
}

export default class LogFileWriterComponent extends Component {
name = 'LogFileWriter'

// Keep track of which log files were requested already. If they're requested
// for the first time this launch, clear them first
private clearedLogFiles: string[] = []

async init() {
await this.addExclusiveProducer(
'logFileWriter:getAndPrepareLogFile',
async (name) => {
const logfile = await this.invokeExclusiveProducer(
'logFile:getLogFile',
name
)

if (!this.clearedLogFiles.includes(name)) {
await logfile.clear()
this.clearedLogFiles.push(name)
}

return logfile
}
)
}
}
80 changes: 80 additions & 0 deletions src/backend/components/core_components/Logger/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Component } from '../../component'

import type { LogLevel, LogMessageType, LogOptions } from './types'

declare module 'backend/components/registry/types' {
interface ComponentEvents {
'logger:logDebug': (message: LogMessageType[], options?: LogOptions) => void
'logger:logInfo': (message: LogMessageType[], options?: LogOptions) => void
'logger:logWarning': (
message: LogMessageType[],
options?: LogOptions
) => void
'logger:logError': (message: LogMessageType[], options?: LogOptions) => void
'logger:logCritical': (
message: LogMessageType[],
options?: LogOptions
) => void
}
}

export default class LoggerComponent extends Component {
name = 'Logger'

private readonly longestLogLevelStrLength = ('CRITICAL' satisfies LogLevel)
.length
private readonly timeFormatter = new Intl.DateTimeFormat('en', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})

async init() {
await Promise.all([
this.addEventListener('logger:logDebug', (message, options) =>
this.logBase('DEBUG', message, options)
),
this.addEventListener('logger:logInfo', (message, options) =>
this.logBase('INFO', message, options)
),
this.addEventListener('logger:logWarning', (message, options) =>
this.logBase('WARNING', message, options)
),
this.addEventListener('logger:logError', (message, options) =>
this.logBase('ERROR', message, options)
),
this.addEventListener('logger:logCritical', (message, options) =>
this.logBase('CRITICAL', message, options)
)
])
}

private async logBase(
severity: LogLevel,
message: LogMessageType[],
options?: LogOptions
) {
// FIXME: Honor the `disableLogs` setting

const formattedTime = this.timeFormatter.format(new Date())
const formattedLevel = `${severity}:`.padEnd(
this.longestLogLevelStrLength + 1
)
const formattedComponent = `[${
options?.componentName || 'Unknown'
}]:`.padEnd(24)
const formattedMessage = this.logMessageToString(message)
const fullMessage = `(${formattedTime}) ${formattedLevel} ${formattedComponent} ${formattedMessage}`

const logfile = await this.invokeExclusiveProducer(
'logFileWriter:getAndPrepareLogFile',
options?.file ?? 'heroic'
)
await logfile.logMessage(fullMessage)
}

private logMessageToString(message: LogMessageType[]): string {
return message.join(', ')
}
}
10 changes: 10 additions & 0 deletions src/backend/components/core_components/Logger/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// TODO: Expand this back to what our pre-component Logger could do
type LogMessageType = string
type LogLevel = 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL'
type LogOptions = Partial<{
componentName: string
file: string
// TODO: Re-add 'showDialog', 'skipLogToFile' and 'forceLog' options
}>

export type { LogMessageType, LogLevel, LogOptions }
13 changes: 13 additions & 0 deletions src/backend/components/core_components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import * as Registry from '../registry'

import LogFileComponent from './LogFile'
import LogFileWriterComponent from './LogFileWriter'
import LoggerComponent from './Logger'

async function registerCoreComponents() {
await Registry.registerComponent(new LogFileComponent())
await Registry.registerComponent(new LogFileWriterComponent())
await Registry.registerComponent(new LoggerComponent())
}

export { registerCoreComponents }
Loading
Loading