revenge-bundle-next/lib/plugins/src/_internal.ts

221 lines
7.1 KiB
TypeScript
Raw Normal View History

2025-05-08 20:25:11 +07:00
import { Logger } from '@revenge-mod/discord/common'
import * as ModulesLibrary from '@revenge-mod/modules'
import * as PatcherLibrary from '@revenge-mod/patcher'
import * as PluginsLibrary from '@revenge-mod/plugins'
import * as ReactLibrary from '@revenge-mod/react'
import * as UtilsLibrary from '@revenge-mod/utils'
2025-05-08 18:52:57 +07:00
import type {
InitPluginApi,
Plugin,
PluginApi,
PluginCleanup,
PluginFlags,
PluginLifecycles,
PluginManifest,
UnscopedInitPluginApi,
UnscopedPluginApi,
} from '../types'
const { PluginFlags: Flag, PluginStatus: Status } = PluginsLibrary
export const _uapi: UnscopedInitPluginApi | UnscopedPluginApi = {
2025-05-08 20:25:11 +07:00
get discord() {
return (this.discord = require('@revenge-mod/discord'))
},
get externals() {
return (this.externals = require('@revenge-mod/externals'))
},
modules: ModulesLibrary,
patcher: PatcherLibrary,
plugins: PluginsLibrary,
react: ReactLibrary,
utils: UtilsLibrary,
}
2025-05-08 18:52:57 +07:00
const _plugins = new Map<PluginManifest['id'], InternalPlugin>()
const _pluginIFlags = new Map<PluginManifest['id'], InternalPluginFlags>()
export function registerPlugin(
2025-05-08 18:52:57 +07:00
manifest: PluginManifest,
lifecycles: PluginLifecycles,
flags: PluginFlags,
iFlags: InternalPluginFlags,
) {
// TODO(plugins): verify plugin manifest
if (_plugins.has(manifest.id)) throw new Error(`Plugin with ID "${manifest.id}" already registered`)
_pluginIFlags.set(manifest.id, iFlags)
const handleError = (e: unknown) => {
plugin.errors.push(e)
2025-05-08 18:52:57 +07:00
plugin.flags |= Flag.Errored
if (plugin.flags & Flag.Enabled) return plugin.disable()
}
const plugin: InternalPlugin = {
promises: [],
cleanups: [],
errors: [],
manifest,
lifecycles,
status: 0,
flags,
prepareStart: () => {
2025-05-08 18:52:57 +07:00
// const sapi = api as PluginApi
// sapi.settings ??= ...
},
2025-04-30 18:53:00 +07:00
async init() {
2025-05-08 18:52:57 +07:00
if (!(plugin.flags & Flag.Enabled)) throw new Error(`Plugin "${manifest.id}" is not enabled`)
if (plugin.status & (Status.Initing | Status.Inited))
throw new Error(`Plugin "${manifest.id}" is initializing or already initialized`)
2025-05-08 18:52:57 +07:00
plugin.status |= Status.Initing
try {
const prom = lifecycles.init?.(api)
plugin.promises.push(prom)
await prom
// plugin.disable() already handles, so it's the try block
2025-05-08 18:52:57 +07:00
plugin.status |= Status.Inited
plugin.status &= ~Status.Initing
} catch (e) {
await handleError(e)
}
},
2025-04-30 18:53:00 +07:00
async start() {
2025-05-08 18:52:57 +07:00
if (!(plugin.flags & Flag.Enabled)) throw new Error(`Plugin "${manifest.id}" is not enabled`)
if (plugin.status & (Status.Starting | Status.Started))
throw new Error(`Plugin "${manifest.id}" is starting or already started`)
// Clear errors from previous runs
plugin.errors = []
2025-05-08 18:52:57 +07:00
plugin.status &= ~Flag.Errored
2025-05-08 18:52:57 +07:00
plugin.status |= Status.Starting
plugin.prepareStart()
try {
2025-05-08 18:52:57 +07:00
const prom = lifecycles.start?.(api as PluginApi)
plugin.promises.push(prom)
await prom
// plugin.disable() already handles, so it's in the try block
2025-05-08 18:52:57 +07:00
plugin.status |= Status.Started
plugin.status &= ~Status.Starting
} catch (e) {
await handleError(e)
}
},
2025-04-30 18:53:00 +07:00
async stop() {
2025-05-08 18:52:57 +07:00
if (!(plugin.flags & Flag.Enabled)) throw new Error(`Plugin "${manifest.id}" is not enabled`)
2025-05-08 18:52:57 +07:00
if (plugin.status & Status.Stopping) throw new Error(`Plugin "${manifest.id}" is stopping`)
// If the plugin is initializing or starting, we need to wait for it to finish, then we'll stop it
2025-05-08 18:52:57 +07:00
if (plugin.status & (Status.Initing | Status.Starting)) await Promise.all(plugin.promises)
2025-05-08 18:52:57 +07:00
if (plugin.status & (Status.Inited | Status.Started))
throw new Error(`Plugin "${manifest.id}" is not initialized started`)
// In case the plugin only has init() and stop()
plugin.prepareStart()
2025-05-08 18:52:57 +07:00
plugin.status |= Status.Stopping
try {
2025-05-08 18:52:57 +07:00
await lifecycles.stop?.(api as PluginApi)
} catch (e) {
await handleError(e)
} finally {
// Run cleanups
const results = await Promise.allSettled(plugin.cleanups.map(cleanup => cleanup()))
for (const result of results)
if (result.status === 'rejected') {
await handleError(result.reason)
// Some cleanup was unsuccessful, so we need to reload the app
2025-05-08 18:52:57 +07:00
plugin.flags |= Flag.ReloadRequired
}
// Clear unnecessary data
plugin.promises = []
plugin.cleanups = []
plugin.status = 0
// Call garbage collector to free up memory
gc()
gc()
}
},
2025-04-30 18:53:00 +07:00
async enable() {
2025-05-08 18:52:57 +07:00
plugin.flags |= Flag.Enabled
},
2025-04-30 18:53:00 +07:00
async disable() {
2025-05-08 18:52:57 +07:00
plugin.flags &= ~Flag.Enabled
// If plugin is not stopped, and is also not stopping, we need to stop it
2025-05-08 18:52:57 +07:00
if (plugin.status && !(plugin.status & Status.Stopping)) await plugin.stop()
},
}
2025-05-08 20:25:11 +07:00
let logger: InstanceType<typeof Logger>
2025-05-08 18:52:57 +07:00
const api: InitPluginApi | PluginApi = {
cleanup: (...items) => {
plugin.cleanups.push(...items)
},
2025-05-08 18:52:57 +07:00
get logger() {
2025-05-08 20:25:11 +07:00
return (logger ??= new Logger(`Revenge > ${manifest.name}`))
2025-05-08 18:52:57 +07:00
},
plugin,
unscoped: _uapi,
}!
_plugins.set(manifest.id, plugin)
}
export function getEnabledPluginsCount() {
let count = 0
2025-05-08 18:52:57 +07:00
for (const plugin of _plugins.values()) if (plugin.flags & Flag.Enabled) count++
return count
}
export async function initPlugins() {
const promises: Promise<void>[] = []
2025-05-08 18:52:57 +07:00
for (const plugin of _plugins.values()) if (plugin.flags & Flag.Enabled) promises.push(plugin.init())
// These Promises never throw, so it is fine
await Promise.all(promises)
}
export async function startPlugins() {
const promises: Promise<void>[] = []
2025-05-08 18:52:57 +07:00
for (const plugin of _plugins.values()) if (plugin.flags & Flag.Enabled) promises.push(plugin.start())
// These Promises never throw, so it is fine
await Promise.all(promises)
}
2025-05-08 18:52:57 +07:00
export interface InternalPlugin extends Plugin {
promises: Promise<void>[]
2025-05-08 18:52:57 +07:00
cleanups: PluginCleanup[]
prepareStart(): void
init(): Promise<void>
start(): Promise<void>
stop(): Promise<void>
}
export const InternalPluginFlags = {
/**
* Marks the plugin as internal.
*/
Internal: 1 << 0,
/**
* Marks the plugin as essential. This means it should not be removed, disabled, or stopped by normal means.
*/
Essential: 1 << 1,
}
export type InternalPluginFlags = (typeof InternalPluginFlags)[keyof typeof InternalPluginFlags]