mirror of
https://github.com/tiramisulabs/seyfert.git
synced 2025-07-02 04:56:07 +00:00
fix: cache flow
This commit is contained in:
parent
bf3db57c71
commit
0da67319f3
1150
src/cache/index.ts
vendored
1150
src/cache/index.ts
vendored
File diff suppressed because it is too large
Load Diff
@ -1,237 +1,217 @@
|
||||
import { GatewayIntentBits, type GatewayDispatchPayload, type GatewayPresenceUpdateData } from 'discord-api-types/v10';
|
||||
import type { Command, CommandContext, Message, SubCommand } from '..';
|
||||
import { lazyLoadPackage, type DeepPartial, type If, type WatcherPayload, type WatcherSendToShard } from '../common';
|
||||
import { EventHandler } from '../events';
|
||||
import { ClientUser } from '../structures';
|
||||
import { ShardManager, properties, type ShardManagerOptions } from '../websocket';
|
||||
import { MemberUpdateHandler } from '../websocket/discord/events/memberUpdate';
|
||||
import { PresenceUpdateHandler } from '../websocket/discord/events/presenceUpdate';
|
||||
import type { BaseClientOptions, InternalRuntimeConfig, ServicesOptions, StartOptions } from './base';
|
||||
import { BaseClient } from './base';
|
||||
import { onInteractionCreate } from './oninteractioncreate';
|
||||
import { onMessageCreate } from './onmessagecreate';
|
||||
import { Collectors } from './collectors';
|
||||
|
||||
let parentPort: import('node:worker_threads').MessagePort;
|
||||
|
||||
export class Client<Ready extends boolean = boolean> extends BaseClient {
|
||||
private __handleGuilds?: Set<string> = new Set();
|
||||
gateway!: ShardManager;
|
||||
me!: If<Ready, ClientUser>;
|
||||
declare options: ClientOptions;
|
||||
memberUpdateHandler = new MemberUpdateHandler();
|
||||
presenceUpdateHandler = new PresenceUpdateHandler();
|
||||
collectors = new Collectors();
|
||||
events? = new EventHandler(this.logger, this.collectors);
|
||||
|
||||
constructor(options?: ClientOptions) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
setServices({
|
||||
gateway,
|
||||
...rest
|
||||
}: ServicesOptions & {
|
||||
gateway?: ShardManager;
|
||||
handlers?: ServicesOptions['handlers'] & {
|
||||
events?: EventHandler['callback'];
|
||||
};
|
||||
}) {
|
||||
super.setServices(rest);
|
||||
if (gateway) {
|
||||
const onPacket = this.onPacket.bind(this);
|
||||
const oldFn = gateway.options.handlePayload;
|
||||
gateway.options.handlePayload = async (shardId, packet) => {
|
||||
await onPacket(shardId, packet);
|
||||
return oldFn(shardId, packet);
|
||||
};
|
||||
this.gateway = gateway;
|
||||
}
|
||||
if (rest.handlers && 'events' in rest.handlers) {
|
||||
if (!rest.handlers.events) {
|
||||
this.events = undefined;
|
||||
} else if (typeof rest.handlers.events === 'function') {
|
||||
this.events = new EventHandler(this.logger, this.collectors);
|
||||
this.events.setHandlers({
|
||||
callback: rest.handlers.events,
|
||||
});
|
||||
} else {
|
||||
this.events = rest.handlers.events;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async loadEvents(dir?: string) {
|
||||
dir ??= await this.getRC().then(x => x.events);
|
||||
if (dir && this.events) {
|
||||
await this.events.load(dir);
|
||||
this.logger.info('EventHandler loaded');
|
||||
}
|
||||
}
|
||||
|
||||
protected async execute(options: { token?: string; intents?: number } = {}) {
|
||||
await super.execute(options);
|
||||
|
||||
const worker_threads = lazyLoadPackage<typeof import('node:worker_threads')>('node:worker_threads');
|
||||
|
||||
if (worker_threads?.parentPort) {
|
||||
parentPort = worker_threads.parentPort;
|
||||
}
|
||||
|
||||
if (worker_threads?.workerData?.__USING_WATCHER__) {
|
||||
parentPort?.on('message', (data: WatcherPayload | WatcherSendToShard) => {
|
||||
switch (data.type) {
|
||||
case 'PAYLOAD':
|
||||
this.gateway.options.handlePayload(data.shardId, data.payload);
|
||||
break;
|
||||
case 'SEND_TO_SHARD':
|
||||
this.gateway.send(data.shardId, data.payload);
|
||||
break;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await this.gateway.spawnShards();
|
||||
}
|
||||
}
|
||||
|
||||
async start(options: Omit<DeepPartial<StartOptions>, 'httpConnection'> = {}, execute = true) {
|
||||
await super.start(options);
|
||||
await this.loadEvents(options.eventsDir);
|
||||
|
||||
const { token: tokenRC, intents: intentsRC, debug: debugRC } = await this.getRC<InternalRuntimeConfig>();
|
||||
const token = options?.token ?? tokenRC;
|
||||
const intents = options?.connection?.intents ?? intentsRC;
|
||||
|
||||
if (!this.gateway) {
|
||||
BaseClient.assertString(token, 'token is not a string');
|
||||
this.gateway = new ShardManager({
|
||||
token,
|
||||
info: await this.proxy.gateway.bot.get(),
|
||||
intents,
|
||||
handlePayload: async (shardId, packet) => {
|
||||
await this.options?.handlePayload?.(shardId, packet);
|
||||
return this.onPacket(shardId, packet);
|
||||
},
|
||||
presence: this.options?.presence,
|
||||
debug: debugRC,
|
||||
shardStart: this.options?.shards?.start,
|
||||
shardEnd: this.options?.shards?.end ?? this.options?.shards?.total,
|
||||
totalShards: this.options?.shards?.total ?? this.options?.shards?.end,
|
||||
properties: { ...this.options?.gateway?.properties, ...properties },
|
||||
compress: this.options?.gateway?.compress,
|
||||
});
|
||||
}
|
||||
|
||||
this.cache.intents = this.gateway.options.intents;
|
||||
|
||||
if (execute) {
|
||||
await this.execute(options.connection);
|
||||
} else {
|
||||
await super.execute(options);
|
||||
}
|
||||
}
|
||||
|
||||
protected async onPacket(shardId: number, packet: GatewayDispatchPayload) {
|
||||
await this.events?.runEvent('RAW', this, packet, shardId);
|
||||
switch (packet.t) {
|
||||
//// Cases where we must obtain the old data before updating
|
||||
case 'GUILD_MEMBER_UPDATE':
|
||||
if (!this.memberUpdateHandler.check(packet.d)) {
|
||||
return;
|
||||
}
|
||||
await this.events?.execute(packet.t, packet, this as Client<true>, shardId);
|
||||
await this.cache.onPacket(packet);
|
||||
break;
|
||||
case 'PRESENCE_UPDATE':
|
||||
if (!this.presenceUpdateHandler.check(packet.d as any)) {
|
||||
return;
|
||||
}
|
||||
await this.events?.execute(packet.t, packet, this as Client<true>, shardId);
|
||||
await this.cache.onPacket(packet);
|
||||
break;
|
||||
|
||||
case 'MESSAGE_UPDATE':
|
||||
case 'MESSAGE_DELETE_BULK':
|
||||
case 'MESSAGE_DELETE':
|
||||
case 'GUILD_DELETE':
|
||||
case 'CHANNEL_UPDATE':
|
||||
case 'GUILD_EMOJIS_UPDATE':
|
||||
case 'GUILD_UPDATE':
|
||||
case 'GUILD_ROLE_UPDATE':
|
||||
case 'GUILD_ROLE_DELETE':
|
||||
case 'THREAD_UPDATE':
|
||||
case 'USER_UPDATE':
|
||||
case 'VOICE_STATE_UPDATE':
|
||||
case 'STAGE_INSTANCE_UPDATE':
|
||||
case 'GUILD_STICKERS_UPDATE':
|
||||
await this.events?.execute(packet.t, packet, this as Client<true>, shardId);
|
||||
await this.cache.onPacket(packet);
|
||||
break;
|
||||
//rest of the events
|
||||
default: {
|
||||
await this.cache.onPacket(packet);
|
||||
await this.events?.execute(packet.t, packet, this as Client<true>, shardId);
|
||||
switch (packet.t) {
|
||||
case 'INTERACTION_CREATE':
|
||||
await onInteractionCreate(this, packet.d, shardId);
|
||||
break;
|
||||
case 'MESSAGE_CREATE':
|
||||
await onMessageCreate(this, packet.d, shardId);
|
||||
break;
|
||||
case 'READY':
|
||||
for (const g of packet.d.guilds) {
|
||||
this.__handleGuilds?.add(g.id);
|
||||
}
|
||||
this.botId = packet.d.user.id;
|
||||
this.applicationId = packet.d.application.id;
|
||||
this.me = new ClientUser(this, packet.d.user, packet.d.application) as never;
|
||||
if (
|
||||
!(
|
||||
this.__handleGuilds?.size &&
|
||||
(this.gateway.options.intents & GatewayIntentBits.Guilds) === GatewayIntentBits.Guilds
|
||||
)
|
||||
) {
|
||||
if ([...this.gateway.values()].every(shard => shard.data.session_id)) {
|
||||
await this.events?.runEvent('BOT_READY', this, this.me, -1);
|
||||
}
|
||||
delete this.__handleGuilds;
|
||||
}
|
||||
this.debugger?.debug(`#${shardId}[${packet.d.user.username}](${this.botId}) is online...`);
|
||||
break;
|
||||
case 'GUILD_CREATE': {
|
||||
if (this.__handleGuilds?.has(packet.d.id)) {
|
||||
this.__handleGuilds.delete(packet.d.id);
|
||||
if (!this.__handleGuilds.size && [...this.gateway.values()].every(shard => shard.data.session_id)) {
|
||||
await this.events?.runEvent('BOT_READY', this, this.me, -1);
|
||||
}
|
||||
if (!this.__handleGuilds.size) delete this.__handleGuilds;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface ClientOptions extends BaseClientOptions {
|
||||
presence?: (shardId: number) => GatewayPresenceUpdateData;
|
||||
shards?: {
|
||||
start: number;
|
||||
end: number;
|
||||
total?: number;
|
||||
};
|
||||
gateway?: {
|
||||
properties?: Partial<ShardManagerOptions['properties']>;
|
||||
compress?: ShardManagerOptions['compress'];
|
||||
};
|
||||
commands?: BaseClientOptions['commands'] & {
|
||||
prefix?: (message: Message) => Promise<string[]> | string[];
|
||||
deferReplyResponse?: (ctx: CommandContext) => Parameters<Message['write']>[0];
|
||||
reply?: (ctx: CommandContext) => boolean;
|
||||
argsParser?: (content: string, command: SubCommand | Command, message: Message) => Record<string, string>;
|
||||
};
|
||||
handlePayload?: ShardManagerOptions['handlePayload'];
|
||||
}
|
||||
import { GatewayIntentBits, type GatewayDispatchPayload, type GatewayPresenceUpdateData } from 'discord-api-types/v10';
|
||||
import type { Command, CommandContext, Message, SubCommand } from '..';
|
||||
import { lazyLoadPackage, type DeepPartial, type If, type WatcherPayload, type WatcherSendToShard } from '../common';
|
||||
import { EventHandler } from '../events';
|
||||
import { ClientUser } from '../structures';
|
||||
import { ShardManager, properties, type ShardManagerOptions } from '../websocket';
|
||||
import { MemberUpdateHandler } from '../websocket/discord/events/memberUpdate';
|
||||
import { PresenceUpdateHandler } from '../websocket/discord/events/presenceUpdate';
|
||||
import type { BaseClientOptions, InternalRuntimeConfig, ServicesOptions, StartOptions } from './base';
|
||||
import { BaseClient } from './base';
|
||||
import { onInteractionCreate } from './oninteractioncreate';
|
||||
import { onMessageCreate } from './onmessagecreate';
|
||||
import { Collectors } from './collectors';
|
||||
|
||||
let parentPort: import('node:worker_threads').MessagePort;
|
||||
|
||||
export class Client<Ready extends boolean = boolean> extends BaseClient {
|
||||
private __handleGuilds?: Set<string> = new Set();
|
||||
gateway!: ShardManager;
|
||||
me!: If<Ready, ClientUser>;
|
||||
declare options: ClientOptions;
|
||||
memberUpdateHandler = new MemberUpdateHandler();
|
||||
presenceUpdateHandler = new PresenceUpdateHandler();
|
||||
collectors = new Collectors();
|
||||
events? = new EventHandler(this);
|
||||
|
||||
constructor(options?: ClientOptions) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
setServices({
|
||||
gateway,
|
||||
...rest
|
||||
}: ServicesOptions & {
|
||||
gateway?: ShardManager;
|
||||
handlers?: ServicesOptions['handlers'] & {
|
||||
events?: EventHandler['callback'];
|
||||
};
|
||||
}) {
|
||||
super.setServices(rest);
|
||||
if (gateway) {
|
||||
const onPacket = this.onPacket.bind(this);
|
||||
const oldFn = gateway.options.handlePayload;
|
||||
gateway.options.handlePayload = async (shardId, packet) => {
|
||||
await onPacket(shardId, packet);
|
||||
return oldFn(shardId, packet);
|
||||
};
|
||||
this.gateway = gateway;
|
||||
}
|
||||
if (rest.handlers && 'events' in rest.handlers) {
|
||||
if (!rest.handlers.events) {
|
||||
this.events = undefined;
|
||||
} else if (typeof rest.handlers.events === 'function') {
|
||||
this.events = new EventHandler(this);
|
||||
this.events.setHandlers({
|
||||
callback: rest.handlers.events,
|
||||
});
|
||||
} else {
|
||||
this.events = rest.handlers.events;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async loadEvents(dir?: string) {
|
||||
dir ??= await this.getRC().then(x => x.events);
|
||||
if (dir && this.events) {
|
||||
await this.events.load(dir);
|
||||
this.logger.info('EventHandler loaded');
|
||||
}
|
||||
}
|
||||
|
||||
protected async execute(options: { token?: string; intents?: number } = {}) {
|
||||
await super.execute(options);
|
||||
|
||||
const worker_threads = lazyLoadPackage<typeof import('node:worker_threads')>('node:worker_threads');
|
||||
|
||||
if (worker_threads?.parentPort) {
|
||||
parentPort = worker_threads.parentPort;
|
||||
}
|
||||
|
||||
if (worker_threads?.workerData?.__USING_WATCHER__) {
|
||||
parentPort?.on('message', (data: WatcherPayload | WatcherSendToShard) => {
|
||||
switch (data.type) {
|
||||
case 'PAYLOAD':
|
||||
this.gateway.options.handlePayload(data.shardId, data.payload);
|
||||
break;
|
||||
case 'SEND_TO_SHARD':
|
||||
this.gateway.send(data.shardId, data.payload);
|
||||
break;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await this.gateway.spawnShards();
|
||||
}
|
||||
}
|
||||
|
||||
async start(options: Omit<DeepPartial<StartOptions>, 'httpConnection'> = {}, execute = true) {
|
||||
await super.start(options);
|
||||
await this.loadEvents(options.eventsDir);
|
||||
|
||||
const { token: tokenRC, intents: intentsRC, debug: debugRC } = await this.getRC<InternalRuntimeConfig>();
|
||||
const token = options?.token ?? tokenRC;
|
||||
const intents = options?.connection?.intents ?? intentsRC;
|
||||
|
||||
if (!this.gateway) {
|
||||
BaseClient.assertString(token, 'token is not a string');
|
||||
this.gateway = new ShardManager({
|
||||
token,
|
||||
info: await this.proxy.gateway.bot.get(),
|
||||
intents,
|
||||
handlePayload: async (shardId, packet) => {
|
||||
await this.options?.handlePayload?.(shardId, packet);
|
||||
return this.onPacket(shardId, packet);
|
||||
},
|
||||
presence: this.options?.presence,
|
||||
debug: debugRC,
|
||||
shardStart: this.options?.shards?.start,
|
||||
shardEnd: this.options?.shards?.end ?? this.options?.shards?.total,
|
||||
totalShards: this.options?.shards?.total ?? this.options?.shards?.end,
|
||||
properties: { ...this.options?.gateway?.properties, ...properties },
|
||||
compress: this.options?.gateway?.compress,
|
||||
});
|
||||
}
|
||||
|
||||
this.cache.intents = this.gateway.options.intents;
|
||||
|
||||
if (execute) {
|
||||
await this.execute(options.connection);
|
||||
} else {
|
||||
await super.execute(options);
|
||||
}
|
||||
}
|
||||
|
||||
protected async onPacket(shardId: number, packet: GatewayDispatchPayload) {
|
||||
// await this.events?.runEvent('RAW', this, packet, shardId);
|
||||
switch (packet.t) {
|
||||
//// Cases where we must obtain the old data before updating
|
||||
case 'GUILD_MEMBER_UPDATE':
|
||||
if (!this.memberUpdateHandler.check(packet.d)) {
|
||||
return;
|
||||
}
|
||||
await this.events?.execute(packet.t, packet, this as Client<true>, shardId);
|
||||
break;
|
||||
case 'PRESENCE_UPDATE':
|
||||
if (!this.presenceUpdateHandler.check(packet.d as any)) {
|
||||
return;
|
||||
}
|
||||
await this.events?.execute(packet.t, packet, this as Client<true>, shardId);
|
||||
break;
|
||||
|
||||
//rest of the events
|
||||
default: {
|
||||
await this.events?.execute(packet.t, packet, this as Client<true>, shardId);
|
||||
switch (packet.t) {
|
||||
case 'INTERACTION_CREATE':
|
||||
await onInteractionCreate(this, packet.d, shardId);
|
||||
break;
|
||||
case 'MESSAGE_CREATE':
|
||||
await onMessageCreate(this, packet.d, shardId);
|
||||
break;
|
||||
case 'READY':
|
||||
for (const g of packet.d.guilds) {
|
||||
this.__handleGuilds?.add(g.id);
|
||||
}
|
||||
this.botId = packet.d.user.id;
|
||||
this.applicationId = packet.d.application.id;
|
||||
this.me = new ClientUser(this, packet.d.user, packet.d.application) as never;
|
||||
if (
|
||||
!(
|
||||
this.__handleGuilds?.size &&
|
||||
(this.gateway.options.intents & GatewayIntentBits.Guilds) === GatewayIntentBits.Guilds
|
||||
)
|
||||
) {
|
||||
if ([...this.gateway.values()].every(shard => shard.data.session_id)) {
|
||||
await this.events?.runEvent('BOT_READY', this, this.me, -1);
|
||||
}
|
||||
delete this.__handleGuilds;
|
||||
}
|
||||
this.debugger?.debug(`#${shardId}[${packet.d.user.username}](${this.botId}) is online...`);
|
||||
break;
|
||||
case 'GUILD_CREATE': {
|
||||
if (this.__handleGuilds?.has(packet.d.id)) {
|
||||
this.__handleGuilds.delete(packet.d.id);
|
||||
if (!this.__handleGuilds.size && [...this.gateway.values()].every(shard => shard.data.session_id)) {
|
||||
await this.events?.runEvent('BOT_READY', this, this.me, -1);
|
||||
}
|
||||
if (!this.__handleGuilds.size) delete this.__handleGuilds;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface ClientOptions extends BaseClientOptions {
|
||||
presence?: (shardId: number) => GatewayPresenceUpdateData;
|
||||
shards?: {
|
||||
start: number;
|
||||
end: number;
|
||||
total?: number;
|
||||
};
|
||||
gateway?: {
|
||||
properties?: Partial<ShardManagerOptions['properties']>;
|
||||
compress?: ShardManagerOptions['compress'];
|
||||
};
|
||||
commands?: BaseClientOptions['commands'] & {
|
||||
prefix?: (message: Message) => Promise<string[]> | string[];
|
||||
deferReplyResponse?: (ctx: CommandContext) => Parameters<Message['write']>[0];
|
||||
reply?: (ctx: CommandContext) => boolean;
|
||||
argsParser?: (content: string, command: SubCommand | Command, message: Message) => Record<string, string>;
|
||||
};
|
||||
handlePayload?: ShardManagerOptions['handlePayload'];
|
||||
}
|
||||
|
@ -1,418 +1,387 @@
|
||||
import { GatewayIntentBits, type GatewayDispatchPayload, type GatewaySendPayload } from 'discord-api-types/v10';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { ApiHandler, Logger } from '..';
|
||||
import type { Cache } from '../cache';
|
||||
import { WorkerAdapter } from '../cache';
|
||||
import { LogLevels, lazyLoadPackage, type DeepPartial, type When } from '../common';
|
||||
import { EventHandler } from '../events';
|
||||
import { ClientUser } from '../structures';
|
||||
import { Shard, type ShardManagerOptions, type WorkerData } from '../websocket';
|
||||
import type {
|
||||
WorkerReady,
|
||||
WorkerReceivePayload,
|
||||
WorkerRequestConnect,
|
||||
WorkerSendEval,
|
||||
WorkerSendEvalResponse,
|
||||
WorkerSendInfo,
|
||||
WorkerSendResultPayload,
|
||||
WorkerSendShardInfo,
|
||||
WorkerShardInfo,
|
||||
WorkerStart,
|
||||
} from '../websocket/discord/worker';
|
||||
import type { ManagerMessages } from '../websocket/discord/workermanager';
|
||||
import type { BaseClientOptions, ServicesOptions, StartOptions } from './base';
|
||||
import { BaseClient } from './base';
|
||||
import type { Client } from './client';
|
||||
import { onInteractionCreate } from './oninteractioncreate';
|
||||
import { onMessageCreate } from './onmessagecreate';
|
||||
import { Collectors } from './collectors';
|
||||
|
||||
let workerData: WorkerData;
|
||||
let manager: import('node:worker_threads').MessagePort;
|
||||
try {
|
||||
workerData = {
|
||||
debug: process.env.SEYFERT_WORKER_DEBUG === 'true',
|
||||
intents: Number.parseInt(process.env.SEYFERT_WORKER_INTENTS!),
|
||||
path: process.env.SEYFERT_WORKER_PATH!,
|
||||
shards: process.env.SEYFERT_WORKER_SHARDS!.split(',').map(id => Number.parseInt(id)),
|
||||
token: process.env.SEYFERT_WORKER_TOKEN!,
|
||||
workerId: Number.parseInt(process.env.SEYFERT_WORKER_WORKERID!),
|
||||
workerProxy: process.env.SEYFERT_WORKER_WORKERPROXY === 'true',
|
||||
} as WorkerData;
|
||||
} catch {}
|
||||
|
||||
export class WorkerClient<Ready extends boolean = boolean> extends BaseClient {
|
||||
private __handleGuilds?: Set<string> = new Set();
|
||||
logger = new Logger({
|
||||
name: `[Worker #${workerData.workerId}]`,
|
||||
});
|
||||
|
||||
collectors = new Collectors();
|
||||
events? = new EventHandler(this.logger, this.collectors);
|
||||
me!: When<Ready, ClientUser>;
|
||||
promises = new Map<string, { resolve: (value: any) => void; timeout: NodeJS.Timeout }>();
|
||||
|
||||
shards = new Map<number, Shard>();
|
||||
|
||||
declare options: WorkerClientOptions;
|
||||
|
||||
constructor(options?: WorkerClientOptions) {
|
||||
super(options);
|
||||
if (!process.env.SEYFERT_SPAWNING) {
|
||||
throw new Error('WorkerClient cannot spawn without manager');
|
||||
}
|
||||
this.postMessage({
|
||||
type: 'WORKER_START',
|
||||
workerId: workerData.workerId,
|
||||
} satisfies WorkerStart);
|
||||
|
||||
const worker_threads = lazyLoadPackage<typeof import('node:worker_threads')>('node:worker_threads');
|
||||
if (worker_threads?.parentPort) {
|
||||
manager = worker_threads?.parentPort;
|
||||
}
|
||||
(manager ?? process).on('message', (data: ManagerMessages) => this.handleManagerMessages(data));
|
||||
|
||||
this.setServices({
|
||||
cache: {
|
||||
adapter: new WorkerAdapter(workerData),
|
||||
disabledCache: options?.disabledCache,
|
||||
},
|
||||
});
|
||||
if (workerData.debug) {
|
||||
this.debugger = new Logger({
|
||||
name: `[Worker #${workerData.workerId}]`,
|
||||
logLevel: LogLevels.Debug,
|
||||
});
|
||||
}
|
||||
if (workerData.workerProxy) {
|
||||
this.setServices({
|
||||
rest: new ApiHandler({
|
||||
token: workerData.token,
|
||||
workerProxy: true,
|
||||
debug: workerData.debug,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get workerId() {
|
||||
return workerData.workerId;
|
||||
}
|
||||
|
||||
get latency() {
|
||||
let acc = 0;
|
||||
|
||||
this.shards.forEach(s => (acc += s.latency));
|
||||
|
||||
return acc / this.shards.size;
|
||||
}
|
||||
|
||||
setServices({
|
||||
...rest
|
||||
}: ServicesOptions & {
|
||||
handlers?: ServicesOptions['handlers'] & {
|
||||
events?: EventHandler['callback'];
|
||||
};
|
||||
}) {
|
||||
super.setServices(rest);
|
||||
if (rest.handlers && 'events' in rest.handlers) {
|
||||
if (!rest.handlers.events) {
|
||||
this.events = undefined;
|
||||
} else if (typeof rest.handlers.events === 'function') {
|
||||
this.events = new EventHandler(this.logger, this.collectors);
|
||||
this.events.setHandlers({
|
||||
callback: rest.handlers.events,
|
||||
});
|
||||
} else {
|
||||
this.events = rest.handlers.events;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async start(options: Omit<DeepPartial<StartOptions>, 'httpConnection' | 'token' | 'connection'> = {}) {
|
||||
await super.start(options);
|
||||
await this.loadEvents(options.eventsDir);
|
||||
this.cache.intents = workerData.intents;
|
||||
}
|
||||
|
||||
async loadEvents(dir?: string) {
|
||||
dir ??= await this.getRC().then(x => x.events);
|
||||
if (dir && this.events) {
|
||||
await this.events.load(dir);
|
||||
this.logger.info('EventHandler loaded');
|
||||
}
|
||||
}
|
||||
|
||||
postMessage(body: any) {
|
||||
if (manager) return manager.postMessage(body);
|
||||
return process.send!(body);
|
||||
}
|
||||
|
||||
protected async handleManagerMessages(data: ManagerMessages) {
|
||||
switch (data.type) {
|
||||
case 'CACHE_RESULT':
|
||||
if (this.cache.adapter instanceof WorkerAdapter && this.cache.adapter.promises.has(data.nonce)) {
|
||||
const cacheData = this.cache.adapter.promises.get(data.nonce)!;
|
||||
clearTimeout(cacheData.timeout);
|
||||
cacheData.resolve(data.result);
|
||||
this.cache.adapter.promises.delete(data.nonce);
|
||||
}
|
||||
break;
|
||||
case 'SEND_PAYLOAD':
|
||||
{
|
||||
const shard = this.shards.get(data.shardId);
|
||||
if (!shard) {
|
||||
this.logger.fatal('Worker trying send payload by non-existent shard');
|
||||
return;
|
||||
}
|
||||
|
||||
await shard.send(true, {
|
||||
...data,
|
||||
} satisfies GatewaySendPayload);
|
||||
|
||||
this.postMessage({
|
||||
type: 'RESULT_PAYLOAD',
|
||||
nonce: data.nonce,
|
||||
workerId: this.workerId,
|
||||
} satisfies WorkerSendResultPayload);
|
||||
}
|
||||
break;
|
||||
case 'ALLOW_CONNECT':
|
||||
{
|
||||
const shard = this.shards.get(data.shardId);
|
||||
if (!shard) {
|
||||
this.logger.fatal('Worker trying connect non-existent shard');
|
||||
return;
|
||||
}
|
||||
shard.options.presence = data.presence;
|
||||
await shard.connect();
|
||||
}
|
||||
break;
|
||||
case 'SPAWN_SHARDS':
|
||||
{
|
||||
const onPacket = this.onPacket.bind(this);
|
||||
const handlePayload = this.options?.handlePayload?.bind(this);
|
||||
const self = this;
|
||||
for (const id of workerData.shards) {
|
||||
let shard = this.shards.get(id);
|
||||
if (!shard) {
|
||||
shard = new Shard(id, {
|
||||
token: workerData.token,
|
||||
intents: workerData.intents,
|
||||
info: data.info,
|
||||
compress: data.compress,
|
||||
debugger: this.debugger,
|
||||
async handlePayload(shardId, payload) {
|
||||
await handlePayload?.(shardId, payload);
|
||||
await self.cache.onPacket(payload);
|
||||
await onPacket?.(payload, shardId);
|
||||
self.postMessage({
|
||||
workerId: workerData.workerId,
|
||||
shardId,
|
||||
type: 'RECEIVE_PAYLOAD',
|
||||
payload,
|
||||
} satisfies WorkerReceivePayload);
|
||||
},
|
||||
});
|
||||
this.shards.set(id, shard);
|
||||
}
|
||||
|
||||
this.postMessage({
|
||||
type: 'CONNECT_QUEUE',
|
||||
shardId: id,
|
||||
workerId: workerData.workerId,
|
||||
} satisfies WorkerRequestConnect);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'SHARD_INFO':
|
||||
{
|
||||
const shard = this.shards.get(data.shardId);
|
||||
if (!shard) {
|
||||
this.logger.fatal('Worker trying get non-existent shard');
|
||||
return;
|
||||
}
|
||||
|
||||
this.postMessage({
|
||||
...generateShardInfo(shard),
|
||||
nonce: data.nonce,
|
||||
type: 'SHARD_INFO',
|
||||
workerId: this.workerId,
|
||||
} satisfies WorkerSendShardInfo);
|
||||
}
|
||||
break;
|
||||
case 'WORKER_INFO':
|
||||
{
|
||||
this.postMessage({
|
||||
shards: [...this.shards.values()].map(generateShardInfo),
|
||||
workerId: workerData.workerId,
|
||||
type: 'WORKER_INFO',
|
||||
nonce: data.nonce,
|
||||
} satisfies WorkerSendInfo);
|
||||
}
|
||||
break;
|
||||
case 'BOT_READY':
|
||||
await this.events?.runEvent('BOT_READY', this, this.me, -1);
|
||||
break;
|
||||
case 'API_RESPONSE':
|
||||
{
|
||||
const promise = this.rest.workerPromises!.get(data.nonce);
|
||||
if (!promise) return;
|
||||
this.rest.workerPromises!.delete(data.nonce);
|
||||
if (data.error) return promise.reject(data.error);
|
||||
promise.resolve(data.response);
|
||||
}
|
||||
break;
|
||||
case 'EXECUTE_EVAL':
|
||||
{
|
||||
let result;
|
||||
try {
|
||||
// biome-ignore lint/security/noGlobalEval: yes
|
||||
result = await eval(`
|
||||
(${data.func})(this)
|
||||
`);
|
||||
} catch (e) {
|
||||
result = e;
|
||||
}
|
||||
this.postMessage({
|
||||
type: 'EVAL_RESPONSE',
|
||||
response: result,
|
||||
workerId: workerData.workerId,
|
||||
nonce: data.nonce,
|
||||
} satisfies WorkerSendEvalResponse);
|
||||
}
|
||||
break;
|
||||
case 'EVAL_RESPONSE':
|
||||
{
|
||||
const evalResponse = this.promises.get(data.nonce);
|
||||
if (!evalResponse) return;
|
||||
this.promises.delete(data.nonce);
|
||||
clearTimeout(evalResponse.timeout);
|
||||
evalResponse.resolve(data.response);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private generateNonce(large = true): string {
|
||||
const uuid = randomUUID();
|
||||
const nonce = large ? uuid : uuid.split('-')[0];
|
||||
if (this.promises.has(nonce)) return this.generateNonce(large);
|
||||
return nonce;
|
||||
}
|
||||
|
||||
private generateSendPromise<T = unknown>(nonce: string, message = 'Timeout'): Promise<T> {
|
||||
return new Promise<T>((res, rej) => {
|
||||
const timeout = setTimeout(() => {
|
||||
this.promises.delete(nonce);
|
||||
rej(new Error(message));
|
||||
}, 60e3);
|
||||
this.promises.set(nonce, { resolve: res, timeout });
|
||||
});
|
||||
}
|
||||
|
||||
tellWorker(workerId: number, func: (_: this) => {}) {
|
||||
const nonce = this.generateNonce();
|
||||
this.postMessage({
|
||||
type: 'EVAL',
|
||||
func: func.toString(),
|
||||
toWorkerId: workerId,
|
||||
workerId: workerData.workerId,
|
||||
nonce,
|
||||
} satisfies WorkerSendEval);
|
||||
return this.generateSendPromise(nonce);
|
||||
}
|
||||
|
||||
protected async onPacket(packet: GatewayDispatchPayload, shardId: number) {
|
||||
await this.events?.execute('RAW', packet, this as WorkerClient<true>, shardId);
|
||||
switch (packet.t) {
|
||||
case 'GUILD_MEMBER_UPDATE':
|
||||
case 'PRESENCE_UPDATE':
|
||||
|
||||
case 'MESSAGE_UPDATE':
|
||||
case 'MESSAGE_DELETE_BULK':
|
||||
case 'MESSAGE_DELETE':
|
||||
case 'GUILD_DELETE':
|
||||
case 'CHANNEL_UPDATE':
|
||||
case 'GUILD_EMOJIS_UPDATE':
|
||||
case 'GUILD_UPDATE':
|
||||
case 'GUILD_ROLE_UPDATE':
|
||||
case 'GUILD_ROLE_DELETE':
|
||||
case 'THREAD_UPDATE':
|
||||
case 'USER_UPDATE':
|
||||
case 'VOICE_STATE_UPDATE':
|
||||
case 'STAGE_INSTANCE_UPDATE':
|
||||
case 'GUILD_STICKERS_UPDATE':
|
||||
await this.events?.execute(packet.t, packet, this as WorkerClient<true>, shardId);
|
||||
await this.cache.onPacket(packet);
|
||||
break;
|
||||
//rest of the events
|
||||
default:
|
||||
{
|
||||
await this.events?.execute(packet.t, packet, this, shardId);
|
||||
switch (packet.t) {
|
||||
case 'READY':
|
||||
for (const g of packet.d.guilds) {
|
||||
this.__handleGuilds?.add(g.id);
|
||||
}
|
||||
this.botId = packet.d.user.id;
|
||||
this.applicationId = packet.d.application.id;
|
||||
this.me = new ClientUser(this, packet.d.user, packet.d.application) as never;
|
||||
if (
|
||||
!(
|
||||
this.__handleGuilds?.size &&
|
||||
(workerData.intents & GatewayIntentBits.Guilds) === GatewayIntentBits.Guilds
|
||||
)
|
||||
) {
|
||||
if ([...this.shards.values()].every(shard => shard.data.session_id)) {
|
||||
this.postMessage({
|
||||
type: 'WORKER_READY',
|
||||
workerId: this.workerId,
|
||||
} as WorkerReady);
|
||||
await this.events?.runEvent('WORKER_READY', this, this.me, -1);
|
||||
}
|
||||
delete this.__handleGuilds;
|
||||
}
|
||||
this.debugger?.debug(`#${shardId} [${packet.d.user.username}](${this.botId}) is online...`);
|
||||
break;
|
||||
case 'INTERACTION_CREATE':
|
||||
await onInteractionCreate(this, packet.d, shardId);
|
||||
break;
|
||||
case 'MESSAGE_CREATE':
|
||||
await onMessageCreate(this, packet.d, shardId);
|
||||
break;
|
||||
case 'GUILD_CREATE': {
|
||||
if (this.__handleGuilds?.has(packet.d.id)) {
|
||||
this.__handleGuilds.delete(packet.d.id);
|
||||
if (!this.__handleGuilds.size && [...this.shards.values()].every(shard => shard.data.session_id)) {
|
||||
this.postMessage({
|
||||
type: 'WORKER_READY',
|
||||
workerId: this.workerId,
|
||||
} as WorkerReady);
|
||||
await this.events?.runEvent('WORKER_READY', this, this.me, -1);
|
||||
}
|
||||
if (!this.__handleGuilds.size) delete this.__handleGuilds;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function generateShardInfo(shard: Shard): WorkerShardInfo {
|
||||
return {
|
||||
open: shard.isOpen,
|
||||
shardId: shard.id,
|
||||
latency: shard.latency,
|
||||
resumable: shard.resumable,
|
||||
};
|
||||
}
|
||||
|
||||
interface WorkerClientOptions extends BaseClientOptions {
|
||||
disabledCache: Cache['disabledCache'];
|
||||
commands?: NonNullable<Client['options']>['commands'];
|
||||
handlePayload?: ShardManagerOptions['handlePayload'];
|
||||
}
|
||||
import { GatewayIntentBits, type GatewayDispatchPayload, type GatewaySendPayload } from 'discord-api-types/v10';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { ApiHandler, Logger } from '..';
|
||||
import type { Cache } from '../cache';
|
||||
import { WorkerAdapter } from '../cache';
|
||||
import { LogLevels, lazyLoadPackage, type DeepPartial, type When } from '../common';
|
||||
import { EventHandler } from '../events';
|
||||
import { ClientUser } from '../structures';
|
||||
import { Shard, type ShardManagerOptions, type WorkerData } from '../websocket';
|
||||
import type {
|
||||
WorkerReady,
|
||||
WorkerReceivePayload,
|
||||
WorkerRequestConnect,
|
||||
WorkerSendEval,
|
||||
WorkerSendEvalResponse,
|
||||
WorkerSendInfo,
|
||||
WorkerSendResultPayload,
|
||||
WorkerSendShardInfo,
|
||||
WorkerShardInfo,
|
||||
WorkerStart,
|
||||
} from '../websocket/discord/worker';
|
||||
import type { ManagerMessages } from '../websocket/discord/workermanager';
|
||||
import type { BaseClientOptions, ServicesOptions, StartOptions } from './base';
|
||||
import { BaseClient } from './base';
|
||||
import type { Client } from './client';
|
||||
import { onInteractionCreate } from './oninteractioncreate';
|
||||
import { onMessageCreate } from './onmessagecreate';
|
||||
import { Collectors } from './collectors';
|
||||
|
||||
let workerData: WorkerData;
|
||||
let manager: import('node:worker_threads').MessagePort;
|
||||
try {
|
||||
workerData = {
|
||||
debug: process.env.SEYFERT_WORKER_DEBUG === 'true',
|
||||
intents: Number.parseInt(process.env.SEYFERT_WORKER_INTENTS!),
|
||||
path: process.env.SEYFERT_WORKER_PATH!,
|
||||
shards: process.env.SEYFERT_WORKER_SHARDS!.split(',').map(id => Number.parseInt(id)),
|
||||
token: process.env.SEYFERT_WORKER_TOKEN!,
|
||||
workerId: Number.parseInt(process.env.SEYFERT_WORKER_WORKERID!),
|
||||
workerProxy: process.env.SEYFERT_WORKER_WORKERPROXY === 'true',
|
||||
} as WorkerData;
|
||||
} catch {}
|
||||
|
||||
export class WorkerClient<Ready extends boolean = boolean> extends BaseClient {
|
||||
private __handleGuilds?: Set<string> = new Set();
|
||||
logger = new Logger({
|
||||
name: `[Worker #${workerData.workerId}]`,
|
||||
});
|
||||
|
||||
collectors = new Collectors();
|
||||
events? = new EventHandler(this);
|
||||
me!: When<Ready, ClientUser>;
|
||||
promises = new Map<string, { resolve: (value: any) => void; timeout: NodeJS.Timeout }>();
|
||||
|
||||
shards = new Map<number, Shard>();
|
||||
|
||||
declare options: WorkerClientOptions;
|
||||
|
||||
constructor(options?: WorkerClientOptions) {
|
||||
super(options);
|
||||
if (!process.env.SEYFERT_SPAWNING) {
|
||||
throw new Error('WorkerClient cannot spawn without manager');
|
||||
}
|
||||
this.postMessage({
|
||||
type: 'WORKER_START',
|
||||
workerId: workerData.workerId,
|
||||
} satisfies WorkerStart);
|
||||
|
||||
const worker_threads = lazyLoadPackage<typeof import('node:worker_threads')>('node:worker_threads');
|
||||
if (worker_threads?.parentPort) {
|
||||
manager = worker_threads?.parentPort;
|
||||
}
|
||||
(manager ?? process).on('message', (data: ManagerMessages) => this.handleManagerMessages(data));
|
||||
|
||||
this.setServices({
|
||||
cache: {
|
||||
adapter: new WorkerAdapter(workerData),
|
||||
disabledCache: options?.disabledCache,
|
||||
},
|
||||
});
|
||||
if (workerData.debug) {
|
||||
this.debugger = new Logger({
|
||||
name: `[Worker #${workerData.workerId}]`,
|
||||
logLevel: LogLevels.Debug,
|
||||
});
|
||||
}
|
||||
if (workerData.workerProxy) {
|
||||
this.setServices({
|
||||
rest: new ApiHandler({
|
||||
token: workerData.token,
|
||||
workerProxy: true,
|
||||
debug: workerData.debug,
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get workerId() {
|
||||
return workerData.workerId;
|
||||
}
|
||||
|
||||
get latency() {
|
||||
let acc = 0;
|
||||
|
||||
this.shards.forEach(s => (acc += s.latency));
|
||||
|
||||
return acc / this.shards.size;
|
||||
}
|
||||
|
||||
setServices({
|
||||
...rest
|
||||
}: ServicesOptions & {
|
||||
handlers?: ServicesOptions['handlers'] & {
|
||||
events?: EventHandler['callback'];
|
||||
};
|
||||
}) {
|
||||
super.setServices(rest);
|
||||
if (rest.handlers && 'events' in rest.handlers) {
|
||||
if (!rest.handlers.events) {
|
||||
this.events = undefined;
|
||||
} else if (typeof rest.handlers.events === 'function') {
|
||||
this.events = new EventHandler(this);
|
||||
this.events.setHandlers({
|
||||
callback: rest.handlers.events,
|
||||
});
|
||||
} else {
|
||||
this.events = rest.handlers.events;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async start(options: Omit<DeepPartial<StartOptions>, 'httpConnection' | 'token' | 'connection'> = {}) {
|
||||
await super.start(options);
|
||||
await this.loadEvents(options.eventsDir);
|
||||
this.cache.intents = workerData.intents;
|
||||
}
|
||||
|
||||
async loadEvents(dir?: string) {
|
||||
dir ??= await this.getRC().then(x => x.events);
|
||||
if (dir && this.events) {
|
||||
await this.events.load(dir);
|
||||
this.logger.info('EventHandler loaded');
|
||||
}
|
||||
}
|
||||
|
||||
postMessage(body: any) {
|
||||
if (manager) return manager.postMessage(body);
|
||||
return process.send!(body);
|
||||
}
|
||||
|
||||
protected async handleManagerMessages(data: ManagerMessages) {
|
||||
switch (data.type) {
|
||||
case 'CACHE_RESULT':
|
||||
if (this.cache.adapter instanceof WorkerAdapter && this.cache.adapter.promises.has(data.nonce)) {
|
||||
const cacheData = this.cache.adapter.promises.get(data.nonce)!;
|
||||
clearTimeout(cacheData.timeout);
|
||||
cacheData.resolve(data.result);
|
||||
this.cache.adapter.promises.delete(data.nonce);
|
||||
}
|
||||
break;
|
||||
case 'SEND_PAYLOAD':
|
||||
{
|
||||
const shard = this.shards.get(data.shardId);
|
||||
if (!shard) {
|
||||
this.logger.fatal('Worker trying send payload by non-existent shard');
|
||||
return;
|
||||
}
|
||||
|
||||
await shard.send(true, {
|
||||
...data,
|
||||
} satisfies GatewaySendPayload);
|
||||
|
||||
this.postMessage({
|
||||
type: 'RESULT_PAYLOAD',
|
||||
nonce: data.nonce,
|
||||
workerId: this.workerId,
|
||||
} satisfies WorkerSendResultPayload);
|
||||
}
|
||||
break;
|
||||
case 'ALLOW_CONNECT':
|
||||
{
|
||||
const shard = this.shards.get(data.shardId);
|
||||
if (!shard) {
|
||||
this.logger.fatal('Worker trying connect non-existent shard');
|
||||
return;
|
||||
}
|
||||
shard.options.presence = data.presence;
|
||||
await shard.connect();
|
||||
}
|
||||
break;
|
||||
case 'SPAWN_SHARDS':
|
||||
{
|
||||
const onPacket = this.onPacket.bind(this);
|
||||
const handlePayload = this.options?.handlePayload?.bind(this);
|
||||
const self = this;
|
||||
for (const id of workerData.shards) {
|
||||
let shard = this.shards.get(id);
|
||||
if (!shard) {
|
||||
shard = new Shard(id, {
|
||||
token: workerData.token,
|
||||
intents: workerData.intents,
|
||||
info: data.info,
|
||||
compress: data.compress,
|
||||
debugger: this.debugger,
|
||||
async handlePayload(shardId, payload) {
|
||||
await handlePayload?.(shardId, payload);
|
||||
await onPacket?.(payload, shardId);
|
||||
self.postMessage({
|
||||
workerId: workerData.workerId,
|
||||
shardId,
|
||||
type: 'RECEIVE_PAYLOAD',
|
||||
payload,
|
||||
} satisfies WorkerReceivePayload);
|
||||
},
|
||||
});
|
||||
this.shards.set(id, shard);
|
||||
}
|
||||
|
||||
this.postMessage({
|
||||
type: 'CONNECT_QUEUE',
|
||||
shardId: id,
|
||||
workerId: workerData.workerId,
|
||||
} satisfies WorkerRequestConnect);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'SHARD_INFO':
|
||||
{
|
||||
const shard = this.shards.get(data.shardId);
|
||||
if (!shard) {
|
||||
this.logger.fatal('Worker trying get non-existent shard');
|
||||
return;
|
||||
}
|
||||
|
||||
this.postMessage({
|
||||
...generateShardInfo(shard),
|
||||
nonce: data.nonce,
|
||||
type: 'SHARD_INFO',
|
||||
workerId: this.workerId,
|
||||
} satisfies WorkerSendShardInfo);
|
||||
}
|
||||
break;
|
||||
case 'WORKER_INFO':
|
||||
{
|
||||
this.postMessage({
|
||||
shards: [...this.shards.values()].map(generateShardInfo),
|
||||
workerId: workerData.workerId,
|
||||
type: 'WORKER_INFO',
|
||||
nonce: data.nonce,
|
||||
} satisfies WorkerSendInfo);
|
||||
}
|
||||
break;
|
||||
case 'BOT_READY':
|
||||
await this.events?.runEvent('BOT_READY', this, this.me, -1);
|
||||
break;
|
||||
case 'API_RESPONSE':
|
||||
{
|
||||
const promise = this.rest.workerPromises!.get(data.nonce);
|
||||
if (!promise) return;
|
||||
this.rest.workerPromises!.delete(data.nonce);
|
||||
if (data.error) return promise.reject(data.error);
|
||||
promise.resolve(data.response);
|
||||
}
|
||||
break;
|
||||
case 'EXECUTE_EVAL':
|
||||
{
|
||||
let result;
|
||||
try {
|
||||
// biome-ignore lint/security/noGlobalEval: yes
|
||||
result = await eval(`
|
||||
(${data.func})(this)
|
||||
`);
|
||||
} catch (e) {
|
||||
result = e;
|
||||
}
|
||||
this.postMessage({
|
||||
type: 'EVAL_RESPONSE',
|
||||
response: result,
|
||||
workerId: workerData.workerId,
|
||||
nonce: data.nonce,
|
||||
} satisfies WorkerSendEvalResponse);
|
||||
}
|
||||
break;
|
||||
case 'EVAL_RESPONSE':
|
||||
{
|
||||
const evalResponse = this.promises.get(data.nonce);
|
||||
if (!evalResponse) return;
|
||||
this.promises.delete(data.nonce);
|
||||
clearTimeout(evalResponse.timeout);
|
||||
evalResponse.resolve(data.response);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private generateNonce(large = true): string {
|
||||
const uuid = randomUUID();
|
||||
const nonce = large ? uuid : uuid.split('-')[0];
|
||||
if (this.promises.has(nonce)) return this.generateNonce(large);
|
||||
return nonce;
|
||||
}
|
||||
|
||||
private generateSendPromise<T = unknown>(nonce: string, message = 'Timeout'): Promise<T> {
|
||||
return new Promise<T>((res, rej) => {
|
||||
const timeout = setTimeout(() => {
|
||||
this.promises.delete(nonce);
|
||||
rej(new Error(message));
|
||||
}, 60e3);
|
||||
this.promises.set(nonce, { resolve: res, timeout });
|
||||
});
|
||||
}
|
||||
|
||||
tellWorker(workerId: number, func: (_: this) => {}) {
|
||||
const nonce = this.generateNonce();
|
||||
this.postMessage({
|
||||
type: 'EVAL',
|
||||
func: func.toString(),
|
||||
toWorkerId: workerId,
|
||||
workerId: workerData.workerId,
|
||||
nonce,
|
||||
} satisfies WorkerSendEval);
|
||||
return this.generateSendPromise(nonce);
|
||||
}
|
||||
|
||||
protected async onPacket(packet: GatewayDispatchPayload, shardId: number) {
|
||||
await this.events?.execute('RAW', packet, this as WorkerClient<true>, shardId);
|
||||
await this.events?.execute(packet.t, packet, this, shardId);
|
||||
switch (packet.t) {
|
||||
case 'READY':
|
||||
for (const g of packet.d.guilds) {
|
||||
this.__handleGuilds?.add(g.id);
|
||||
}
|
||||
this.botId = packet.d.user.id;
|
||||
this.applicationId = packet.d.application.id;
|
||||
this.me = new ClientUser(this, packet.d.user, packet.d.application) as never;
|
||||
if (
|
||||
!(this.__handleGuilds?.size && (workerData.intents & GatewayIntentBits.Guilds) === GatewayIntentBits.Guilds)
|
||||
) {
|
||||
if ([...this.shards.values()].every(shard => shard.data.session_id)) {
|
||||
this.postMessage({
|
||||
type: 'WORKER_READY',
|
||||
workerId: this.workerId,
|
||||
} as WorkerReady);
|
||||
await this.events?.runEvent('WORKER_READY', this, this.me, -1);
|
||||
}
|
||||
delete this.__handleGuilds;
|
||||
}
|
||||
this.debugger?.debug(`#${shardId} [${packet.d.user.username}](${this.botId}) is online...`);
|
||||
break;
|
||||
case 'INTERACTION_CREATE':
|
||||
await onInteractionCreate(this, packet.d, shardId);
|
||||
break;
|
||||
case 'MESSAGE_CREATE':
|
||||
await onMessageCreate(this, packet.d, shardId);
|
||||
break;
|
||||
case 'GUILD_CREATE': {
|
||||
if (this.__handleGuilds?.has(packet.d.id)) {
|
||||
this.__handleGuilds.delete(packet.d.id);
|
||||
if (!this.__handleGuilds.size && [...this.shards.values()].every(shard => shard.data.session_id)) {
|
||||
this.postMessage({
|
||||
type: 'WORKER_READY',
|
||||
workerId: this.workerId,
|
||||
} as WorkerReady);
|
||||
await this.events?.runEvent('WORKER_READY', this, this.me, -1);
|
||||
}
|
||||
if (!this.__handleGuilds.size) delete this.__handleGuilds;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function generateShardInfo(shard: Shard): WorkerShardInfo {
|
||||
return {
|
||||
open: shard.isOpen,
|
||||
shardId: shard.id,
|
||||
latency: shard.latency,
|
||||
resumable: shard.resumable,
|
||||
};
|
||||
}
|
||||
|
||||
interface WorkerClientOptions extends BaseClientOptions {
|
||||
disabledCache: Cache['disabledCache'];
|
||||
commands?: NonNullable<Client['options']>['commands'];
|
||||
handlePayload?: ShardManagerOptions['handlePayload'];
|
||||
}
|
||||
|
@ -1,129 +1,136 @@
|
||||
import type {
|
||||
GatewayDispatchPayload,
|
||||
GatewayMessageCreateDispatch,
|
||||
GatewayMessageDeleteBulkDispatch,
|
||||
GatewayMessageDeleteDispatch,
|
||||
} from 'discord-api-types/v10';
|
||||
import type { Client, WorkerClient } from '../client';
|
||||
import { BaseHandler, type Logger, ReplaceRegex, magicImport, type MakeRequired, type SnakeCase } from '../common';
|
||||
import type { ClientEvents } from '../events/hooks';
|
||||
import * as RawEvents from '../events/hooks';
|
||||
import type { ClientEvent, ClientNameEvents } from './event';
|
||||
import type { Collectors } from '../client/collectors';
|
||||
|
||||
export type EventValue = MakeRequired<ClientEvent, '__filePath'> & { fired?: boolean };
|
||||
|
||||
export type GatewayEvents = Uppercase<SnakeCase<keyof ClientEvents>>;
|
||||
|
||||
export class EventHandler extends BaseHandler {
|
||||
constructor(
|
||||
logger: Logger,
|
||||
protected collectors: Collectors,
|
||||
) {
|
||||
super(logger);
|
||||
}
|
||||
|
||||
onFail = (event: GatewayEvents, err: unknown) => this.logger.warn('<Client>.events.onFail', err, event);
|
||||
protected filter = (path: string) => path.endsWith('.js') || (!path.endsWith('.d.ts') && path.endsWith('.ts'));
|
||||
|
||||
values: Partial<Record<GatewayEvents, EventValue>> = {};
|
||||
|
||||
async load(eventsDir: string, instances?: { file: ClientEvent; path: string }[]) {
|
||||
for (const i of instances ?? (await this.loadFilesK<ClientEvent>(await this.getFiles(eventsDir)))) {
|
||||
const instance = this.callback(i.file);
|
||||
if (!instance) continue;
|
||||
if (typeof instance?.run !== 'function') {
|
||||
this.logger.warn(
|
||||
i.path.split(process.cwd()).slice(1).join(process.cwd()),
|
||||
'Missing run function, use `export default {...}` syntax',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
instance.__filePath = i.path;
|
||||
this.values[ReplaceRegex.snake(instance.data.name).toUpperCase() as GatewayEvents] = instance as EventValue;
|
||||
}
|
||||
}
|
||||
|
||||
async execute(name: GatewayEvents, ...args: [GatewayDispatchPayload, Client<true> | WorkerClient<true>, number]) {
|
||||
switch (name) {
|
||||
case 'MESSAGE_CREATE':
|
||||
{
|
||||
const { d: data } = args[0] as GatewayMessageCreateDispatch;
|
||||
if (args[1].components?.values.has(data.interaction_metadata?.id ?? data.id)) {
|
||||
args[1].components.values.get(data.interaction_metadata?.id ?? data.id)!.messageId = data.id;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'MESSAGE_DELETE':
|
||||
{
|
||||
const { d: data } = args[0] as GatewayMessageDeleteDispatch;
|
||||
const value = [...(args[1].components?.values ?? [])].find(x => x[1].messageId === data.id);
|
||||
if (value) {
|
||||
args[1].components!.onMessageDelete(value[0]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'MESSAGE_DELETE_BULK':
|
||||
{
|
||||
const { d: data } = args[0] as GatewayMessageDeleteBulkDispatch;
|
||||
const values = [...(args[1].components?.values ?? [])];
|
||||
data.ids.forEach(id => {
|
||||
const value = values.find(x => x[1].messageId === id);
|
||||
if (value) {
|
||||
args[1].components!.onMessageDelete(value[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
await this.runEvent(args[0].t, args[1], args[0].d, args[2]);
|
||||
await this.collectors.run(args[0].t, args[0].d);
|
||||
}
|
||||
|
||||
async runEvent(name: GatewayEvents, client: Client | WorkerClient, packet: any, shardId: number) {
|
||||
const Event = this.values[name];
|
||||
if (!Event) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (Event.data.once && Event.fired) {
|
||||
return;
|
||||
}
|
||||
Event.fired = true;
|
||||
const hook = await RawEvents[name]?.(client, packet as never);
|
||||
await Event.run(...[hook, client, shardId]);
|
||||
} catch (e) {
|
||||
await this.onFail(name, e);
|
||||
}
|
||||
}
|
||||
|
||||
async reload(name: ClientNameEvents) {
|
||||
const eventName = ReplaceRegex.snake(name).toUpperCase() as GatewayEvents;
|
||||
const event = this.values[eventName];
|
||||
if (!event?.__filePath) return null;
|
||||
delete require.cache[event.__filePath];
|
||||
const imported = await magicImport(event.__filePath).then(x => x.default ?? x);
|
||||
imported.__filePath = event.__filePath;
|
||||
this.values[eventName] = imported;
|
||||
return imported;
|
||||
}
|
||||
|
||||
async reloadAll(stopIfFail = true) {
|
||||
for (const i in this.values) {
|
||||
try {
|
||||
await this.reload(ReplaceRegex.camel(i) as ClientNameEvents);
|
||||
} catch (e) {
|
||||
if (stopIfFail) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setHandlers({ callback }: { callback: EventHandler['callback'] }) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
callback = (file: ClientEvent): ClientEvent | false => file;
|
||||
}
|
||||
import type {
|
||||
GatewayDispatchPayload,
|
||||
GatewayMessageCreateDispatch,
|
||||
GatewayMessageDeleteBulkDispatch,
|
||||
GatewayMessageDeleteDispatch,
|
||||
} from 'discord-api-types/v10';
|
||||
import type { Client, WorkerClient } from '../client';
|
||||
import { BaseHandler, ReplaceRegex, magicImport, type MakeRequired, type SnakeCase } from '../common';
|
||||
import type { ClientEvents } from '../events/hooks';
|
||||
import * as RawEvents from '../events/hooks';
|
||||
import type { ClientEvent, ClientNameEvents } from './event';
|
||||
|
||||
export type EventValue = MakeRequired<ClientEvent, '__filePath'> & { fired?: boolean };
|
||||
|
||||
export type GatewayEvents = Uppercase<SnakeCase<keyof ClientEvents>>;
|
||||
|
||||
export class EventHandler extends BaseHandler {
|
||||
constructor(protected client: Client | WorkerClient) {
|
||||
super(client.logger);
|
||||
}
|
||||
|
||||
onFail = (event: GatewayEvents, err: unknown) => this.logger.warn('<Client>.events.onFail', err, event);
|
||||
protected filter = (path: string) => path.endsWith('.js') || (!path.endsWith('.d.ts') && path.endsWith('.ts'));
|
||||
|
||||
values: Partial<Record<GatewayEvents, EventValue>> = {};
|
||||
|
||||
async load(eventsDir: string, instances?: { file: ClientEvent; path: string }[]) {
|
||||
for (const i of instances ?? (await this.loadFilesK<ClientEvent>(await this.getFiles(eventsDir)))) {
|
||||
const instance = this.callback(i.file);
|
||||
if (!instance) continue;
|
||||
if (typeof instance?.run !== 'function') {
|
||||
this.logger.warn(
|
||||
i.path.split(process.cwd()).slice(1).join(process.cwd()),
|
||||
'Missing run function, use `export default {...}` syntax',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
instance.__filePath = i.path;
|
||||
this.values[ReplaceRegex.snake(instance.data.name).toUpperCase() as GatewayEvents] = instance as EventValue;
|
||||
}
|
||||
}
|
||||
|
||||
async execute(name: GatewayEvents, ...args: [GatewayDispatchPayload, Client<true> | WorkerClient<true>, number]) {
|
||||
switch (name) {
|
||||
case 'MESSAGE_CREATE':
|
||||
{
|
||||
const { d: data } = args[0] as GatewayMessageCreateDispatch;
|
||||
if (args[1].components?.values.has(data.interaction_metadata?.id ?? data.id)) {
|
||||
args[1].components.values.get(data.interaction_metadata?.id ?? data.id)!.messageId = data.id;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'MESSAGE_DELETE':
|
||||
{
|
||||
const { d: data } = args[0] as GatewayMessageDeleteDispatch;
|
||||
const value = [...(args[1].components?.values ?? [])].find(x => x[1].messageId === data.id);
|
||||
if (value) {
|
||||
args[1].components!.onMessageDelete(value[0]);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'MESSAGE_DELETE_BULK':
|
||||
{
|
||||
const { d: data } = args[0] as GatewayMessageDeleteBulkDispatch;
|
||||
const values = [...(args[1].components?.values ?? [])];
|
||||
data.ids.forEach(id => {
|
||||
const value = values.find(x => x[1].messageId === id);
|
||||
if (value) {
|
||||
args[1].components!.onMessageDelete(value[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
await this.runEvent(args[0].t, args[1], args[0].d, args[2]);
|
||||
await this.client.collectors.run(args[0].t, args[0].d);
|
||||
}
|
||||
|
||||
async runEvent(name: GatewayEvents, client: Client | WorkerClient, packet: any, shardId: number) {
|
||||
const Event = this.values[name];
|
||||
if (!Event) {
|
||||
return this.client.cache.onPacket({
|
||||
t: name,
|
||||
d: packet,
|
||||
} as GatewayDispatchPayload);
|
||||
}
|
||||
try {
|
||||
if (Event.data.once && Event.fired) {
|
||||
return this.client.cache.onPacket({
|
||||
t: name,
|
||||
d: packet,
|
||||
} as GatewayDispatchPayload);
|
||||
}
|
||||
Event.fired = true;
|
||||
const hook = await RawEvents[name]?.(client, packet as never);
|
||||
if (name !== 'RAW')
|
||||
await this.client.cache.onPacket({
|
||||
t: name,
|
||||
d: packet,
|
||||
} as GatewayDispatchPayload);
|
||||
await Event.run(...[hook, client, shardId]);
|
||||
} catch (e) {
|
||||
await this.onFail(name, e);
|
||||
}
|
||||
}
|
||||
|
||||
async reload(name: ClientNameEvents) {
|
||||
const eventName = ReplaceRegex.snake(name).toUpperCase() as GatewayEvents;
|
||||
const event = this.values[eventName];
|
||||
if (!event?.__filePath) return null;
|
||||
delete require.cache[event.__filePath];
|
||||
const imported = await magicImport(event.__filePath).then(x => x.default ?? x);
|
||||
imported.__filePath = event.__filePath;
|
||||
this.values[eventName] = imported;
|
||||
return imported;
|
||||
}
|
||||
|
||||
async reloadAll(stopIfFail = true) {
|
||||
for (const i in this.values) {
|
||||
try {
|
||||
await this.reload(ReplaceRegex.camel(i) as ClientNameEvents);
|
||||
} catch (e) {
|
||||
if (stopIfFail) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setHandlers({ callback }: { callback: EventHandler['callback'] }) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
callback = (file: ClientEvent): ClientEvent | false => file;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user