seyfert/src/common/bot/watcher.ts
MARCROCK22 3b2cbd3089
Feat: cloudfare workers support (#190)
* feat: cloudfare workers

* fix: chokidar

* fix: handlers

* fix: lazyLoadPackage
2024-05-07 16:53:10 -04:00

144 lines
3.8 KiB
TypeScript

import type { GatewayDispatchPayload, GatewaySendPayload } from 'discord-api-types/v10';
import { execSync } from 'node:child_process';
import { ApiHandler, Router } from '../../api';
import { BaseClient, type InternalRuntimeConfig } from '../../client/base';
import { ShardManager, type ShardManagerOptions } from '../../websocket';
import { Logger } from '../it/logger';
import type { MakeRequired } from '../types/util';
import { lazyLoadPackage } from '../it/utils';
/**
* Represents a watcher class that extends the ShardManager.
*/
export class Watcher extends ShardManager {
worker?: import('node:worker_threads').Worker;
logger = new Logger({
name: '[Watcher]',
});
rest?: ApiHandler;
declare options: MakeRequired<WatcherOptions, 'intents' | 'token' | 'handlePayload' | 'info'>;
/**
* Initializes a new instance of the Watcher class.
* @param options The options for the watcher.
*/
constructor(options: WatcherOptions) {
super({
handlePayload() {},
token: '',
intents: 0,
info: {
url: 'wss://gateway.discord.gg',
session_start_limit: {
max_concurrency: -1,
remaining: -1,
reset_after: -1,
total: -1,
},
shards: -1,
},
...options,
});
}
/**
* Resets the worker instance.
*/
resetWorker() {
const worker_threads = lazyLoadPackage<typeof import('node:worker_threads')>('node:worker_threads');
if (!worker_threads) throw new Error('Cannot use worker_threads');
if (this.worker) {
this.worker.terminate();
}
this.build();
this.worker = new worker_threads.Worker(this.options.filePath, {
argv: this.options.argv,
workerData: {
__USING_WATCHER__: true,
},
});
this.worker!.on('message', (data: WatcherSendToShard) => {
switch (data.type) {
case 'SEND_TO_SHARD':
this.send(data.shardId, data.payload);
break;
}
});
}
/**
* Spawns shards for the watcher.
*/
async spawnShards() {
const RC = await BaseClient.prototype.getRC<InternalRuntimeConfig>();
this.options.token = RC.token;
this.rest ??= new ApiHandler({
baseUrl: 'api/v10',
domain: 'https://discord.com',
token: this.options.token,
});
this.options.intents = RC.intents;
this.options.info = await new Router(this.rest!).createProxy().gateway.bot.get();
this.options.totalShards = this.options.info.shards;
this.resetWorker();
const oldFn = this.options.handlePayload;
this.options.handlePayload = (shardId, payload) => {
this.worker?.postMessage({
type: 'PAYLOAD',
shardId,
payload,
} satisfies WatcherPayload);
return oldFn?.(shardId, payload);
};
this.connectQueue.concurrency = this.options.info.session_start_limit.max_concurrency;
await super.spawnShards();
const chokidar = lazyLoadPackage<typeof import('chokidar')>('chokidar');
if (!chokidar?.watch) return this.logger.warn('No chokidar installed.');
const watcher = chokidar.watch(this.options.srcPath).on('ready', () => {
this.logger.debug(`Watching ${this.options.srcPath}`);
watcher.on('all', event => {
this.logger.debug(`${event} event detected, building`);
this.resetWorker();
});
});
}
/**
* Builds the watcher.
*/
protected build() {
execSync(`cd ${process.cwd()} && ${this.options.transpileCommand}`);
this.logger.info('Builded');
}
}
export interface WatcherOptions extends Omit<ShardManagerOptions, 'handlePayload' | 'info' | 'token' | 'intents'> {
filePath: string;
transpileCommand: string;
srcPath: string;
argv?: string[];
handlePayload?: ShardManagerOptions['handlePayload'];
info?: ShardManagerOptions['info'];
token?: ShardManagerOptions['token'];
intents?: ShardManagerOptions['intents'];
}
export interface WatcherPayload {
type: 'PAYLOAD';
shardId: number;
payload: GatewayDispatchPayload;
}
export interface WatcherSendToShard {
type: 'SEND_TO_SHARD';
shardId: number;
payload: GatewaySendPayload;
}