import {performance} from 'node:perf_hooks'; import type {Bot} from '@wp2508/mineflayer'; import type {BaseInfo, InfoI18n} from './types.ts'; type Meter = {depth: number; total: number}; const meters = new WeakMap(); /** Only the synchronous invocation is timed, even when it returns a Promise. */ export function measure_sync(bot: object, invoke: () => T): T { const meter = meters.get(bot); if (!meter) return invoke(); const outer = meter.depth++ === 0; const start = outer ? performance.now() : 0; try { return invoke(); } finally { meter.depth--; if (outer) meter.total += performance.now() - start; } } export function instrument_bot(bot: Bot): {total: () => number; dispose: () => void} { if (meters.has(bot)) throw new Error('Bot resource meter already installed'); const meter: Meter = {depth: 0, total: 0}; meters.set(bot, meter); const restore: (() => void)[] = []; try { for (const target of new Set([bot, bot._client])) { if (!target || typeof target.emit !== 'function') continue; const descriptor = Object.getOwnPropertyDescriptor(target, 'emit'); const original = target.emit; const wrapper = function(this: typeof target, ...args: Parameters) { return measure_sync(bot, () => Reflect.apply(original, this, args)); }; Object.defineProperty(target, 'emit', {configurable: true, writable: true, enumerable: descriptor?.enumerable ?? false, value: wrapper}); restore.push(() => { if (Object.getOwnPropertyDescriptor(target, 'emit')?.value !== wrapper) return; if (descriptor) Object.defineProperty(target, 'emit', descriptor); else delete (target as unknown as {emit?: unknown}).emit; }); } } catch (error) { for (const undo of restore.reverse()) undo(); meters.delete(bot); throw error; } let disposed = false; return {total: () => meter.total, dispose: () => { if (disposed) return; disposed = true; meters.delete(bot); for (const undo of restore.reverse()) undo(); }}; } export function metadata(name: string, description: string, fields: Record): InfoI18n { return {lang: 'zh-CN', name, description, fields: Object.fromEntries(Object.entries(fields).map( ([key, [name, description]]) => [key, {lang: 'zh-CN', name, description}], ))}; } /** No module owns bot end listeners; InfoManager owns disposal. */ export abstract class ResourceInfo implements BaseInfo { abstract readonly name: string; abstract readonly i18n: InfoI18n; protected disposed = false; private timer?: ReturnType; private running = false; private readonly listeners = new Set<(value: T) => void>(); constructor(protected info: T) {} get_info(): T { return this.info; } on_update(callback: (value: T) => void): () => void { if (this.disposed) throw new Error('Resource info disposed'); this.listeners.add(callback); return () => { this.listeners.delete(callback); }; } protected publish(info: T): void { if (this.disposed) return; this.info = info; for (const listener of Array.from(this.listeners)) { if (this.disposed) break; try { listener(info); } catch (error) { console.error('Resource info listener failed', error); } } } protected start(interval: number, sample: () => void | Promise, immediate = false): void { const run = async () => { if (this.running || this.disposed) return; this.running = true; try { await sample(); } catch (error) { if (!this.disposed) console.error('Resource sampling failed', error); } finally { this.running = false; } }; this.timer = setInterval(() => { void run(); }, interval); this.timer.unref?.(); if (immediate) void run(); } dispose(): void { if (this.disposed) return; this.disposed = true; clearInterval(this.timer); this.listeners.clear(); } }