feat(lib/plugins): add API for storages

This commit is contained in:
PalmDevs 2025-05-22 02:45:38 +07:00
parent 3c23236273
commit 7a1ff7ef0c
No known key found for this signature in database
GPG key ID: 6FAE8F8677ACA8BA
3 changed files with 74 additions and 21 deletions

View file

@ -1,16 +1,18 @@
import { getStorage, type StorageOptions } from '@revenge-mod/storage'
import { allSettled, sleepReject } from '@revenge-mod/utils/promises'
import { _uapi } from './apis'
import { PluginFlags as Flag, PluginStatus as Status } from './constants'
import { PluginFlags as Flag, PluginsStorageDirectory, PluginStatus as Status } from './constants'
import type { AnyObject } from '@revenge-mod/utils/types'
import type {
InitPluginApi,
Plugin,
PluginApi,
PluginCleanup,
PluginLifecycles,
PluginManifest,
PluginOptions,
PreInitPluginApi,
} from './types'
@ -25,15 +27,26 @@ export const _metas = new Map<
]
>()
export function registerPlugin(manifest: PluginManifest, lifecycles: PluginLifecycles, flags: number, iflags: number) {
export function registerPlugin<S extends AnyObject = AnyObject>(
manifest: PluginManifest,
options: PluginOptions<S>,
flags: number,
iflags: number,
) {
// TODO(plugins): verify plugin manifest
if (_plugins.has(manifest.id)) throw new Error(`Plugin with ID "${manifest.id}" already registered`)
const plugin: InternalPlugin = {
cleanups: [],
_c: [],
_s: options.storage,
errors: [],
manifest,
lifecycles,
lifecycles: {
preInit: options.preInit,
init: options.init,
start: options.start,
stop: options.stop,
},
status: 0,
flags,
disable: () => disablePlugin(plugin),
@ -65,7 +78,7 @@ function preparePluginPreInit(id: PluginManifest['id']) {
meta[0] = {
cleanup: (...items) => {
plugin.cleanups.push(...items)
plugin._c.push(...items)
},
plugin,
unscoped: _uapi,
@ -74,11 +87,33 @@ function preparePluginPreInit(id: PluginManifest['id']) {
meta[3] = PluginApiLevel.PreInit
}
function preparePluginInit(id: PluginManifest['id']) {
function preparePluginInit(id: PluginManifest['id'], storageOptions?: StorageOptions<AnyObject>) {
const meta = _metas.get(id)!
const api = meta[0] as InitPluginApi
api.logger = new api.unscoped.discord.common.Logger(`Revenge > Plugins (${id})`)
Object.defineProperties(api, {
logger: {
configurable: true,
get() {
// @ts-expect-error
// biome-ignore lint/performance/noDelete: <explanation>
delete api.logger
return (api.logger = new api.unscoped.discord.common.Logger(`Revenge > Plugins (${id})`))
},
},
storage: {
configurable: true,
get() {
// @ts-expect-error
// biome-ignore lint/performance/noDelete: <explanation>
delete api.storage
return (api.storage = getStorage(`${PluginsStorageDirectory}/${id}.json`, {
...storageOptions,
directory: 'documents',
}))
},
},
})
meta[3] = PluginApiLevel.Init
}
@ -224,7 +259,7 @@ export async function stopPlugin(plugin: InternalPlugin) {
throw new Error(`Plugin "${manifest.id}" is not running`)
if (apiLevel < PluginApiLevel.PreInit) preparePluginPreInit(manifest.id)
if (apiLevel < PluginApiLevel.Init) preparePluginInit(manifest.id)
if (apiLevel < PluginApiLevel.Init) preparePluginInit(manifest.id, plugin._s)
if (apiLevel < PluginApiLevel.Start) preparePluginStart(manifest.id)
plugin.status |= Status.Stopping
@ -239,7 +274,7 @@ export async function stopPlugin(plugin: InternalPlugin) {
await handlePluginError(e, plugin)
} finally {
// Run cleanups
const results = await allSettled(plugin.cleanups.map(cleanup => cleanup()))
const results = await allSettled(plugin._c.map(cleanup => cleanup()))
for (const result of results)
if (result.status === 'rejected') {
await handlePluginError(result.reason, plugin)
@ -253,7 +288,7 @@ export async function stopPlugin(plugin: InternalPlugin) {
// Clear temp data
meta[1] = []
plugin.cleanups = []
plugin._c = []
// Reset status
plugin.status = 0
@ -261,7 +296,8 @@ export async function stopPlugin(plugin: InternalPlugin) {
}
export interface InternalPlugin extends Plugin {
cleanups: PluginCleanup[]
_s?: StorageOptions<AnyObject>
_c: PluginCleanup[]
}
export const InternalPluginFlags = {

View file

@ -27,3 +27,5 @@ export const PluginStatus = {
Started: 1 << 5,
Stopping: 1 << 6,
}
export const PluginsStorageDirectory = 'revenge/plugins/storage'

View file

@ -2,6 +2,9 @@ import type { PluginFlags, PluginStatus } from './constants'
import type { DiscordModules } from '@revenge-mod/discord/types'
import type { Storage, StorageOptions } from '@revenge-mod/storage'
import type { AnyObject } from '@revenge-mod/utils/types'
import type { PluginApiDiscord } from './apis/discord'
import type { PluginApiExternals } from './apis/externals'
import type { PluginApiModules } from './apis/modules'
@ -55,17 +58,25 @@ export interface PreInitPluginApi {
* The plugin API (limited).
* Available in the `init` phase.
*/
export interface InitPluginApi extends PreInitPluginApi {
export interface InitPluginApi<S extends AnyObject = AnyObject> extends PreInitPluginApi {
unscoped: UnscopedInitPluginApi
logger: InstanceType<DiscordModules.Logger>
/**
* The plugin storage.
*
* Note that the instance is only created when the plugin accesses the API.
* This is to prevent unnecessary storage instances from being created.
*
* To preload storage, simply call `api.storage.get()`.
*/
storage: Storage<S>
}
/**
* The plugin API.
* Available in the `start` and `stop` phase.
*/
export interface PluginApi extends InitPluginApi {
// settings: typeof import('@revenge-mod/settings')
export interface PluginApi<S extends AnyObject = AnyObject> extends InitPluginApi<S> {
unscoped: UnscopedPluginApi
}
@ -108,7 +119,11 @@ export interface PluginManifest {
icon?: string
}
export interface PluginLifecycles {
export interface PluginOptions<S extends AnyObject = AnyObject> extends PluginLifecycles<S> {
storage?: Omit<StorageOptions<S>, 'directory'>
}
export interface PluginLifecycles<S extends AnyObject = AnyObject> {
/**
* Runs as soon as possible with very limited APIs.
* Before the index module (module 0)'s factory is run.
@ -122,22 +137,22 @@ export interface PluginLifecycles {
*
* @param api Plugin API (limited).
*/
init?: (api: InitPluginApi) => any
init?: (api: InitPluginApi<S>) => any
/**
* Runs when the plugin can be started with all APIs available.
*
* @param api Plugin API.
*/
start?: (api: PluginApi) => any
start?: (api: PluginApi<S>) => any
/**
* Runs when the plugin is stopped.
*
* @param api Plugin API.
*/
stop?: (api: PluginApi) => any
stop?: (api: PluginApi<S>) => any
}
export interface Plugin {
export interface Plugin<S extends AnyObject = AnyObject> {
// TODO(plugins): support plugin bundles
// /**
// * The plugin bundle this plugin belongs to.
@ -150,7 +165,7 @@ export interface Plugin {
/**
* The plugin lifecycles.
*/
lifecycles: PluginLifecycles
lifecycles: PluginLifecycles<S>
/**
* The plugin flags.