RESTManager, CacheAdapters, and improvements
This commit is contained in:
		
							parent
							
								
									f319e0df91
								
							
						
					
					
						commit
						935456906d
					
				
					 26 changed files with 770 additions and 57 deletions
				
			
		|  | @ -8,6 +8,7 @@ export const channelCreate: GatewayEventHandler = ( | |||
|   const channel = getChannelByType(gateway.client, d) | ||||
| 
 | ||||
|   if (channel !== undefined) { | ||||
|     gateway.client.channels.set(d.id, d) | ||||
|     gateway.client.emit('channelCreate', channel) | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -1,14 +1,13 @@ | |||
| import { Gateway, GatewayEventHandler } from '../index.ts' | ||||
| import cache from '../../models/cache.ts' | ||||
| import { Channel } from '../../structures/channel.ts' | ||||
| 
 | ||||
| export const channelDelete: GatewayEventHandler = ( | ||||
|   gateway: Gateway, | ||||
|   d: any | ||||
| ) => { | ||||
|   const channel: Channel = cache.get('channel', d.id) | ||||
|   const channel: Channel = gateway.client.channels.get(d.id) | ||||
|   if (channel !== undefined) { | ||||
|     cache.del('channel', d.id) | ||||
|     gateway.client.channels.delete(d.id) | ||||
|     gateway.client.emit('channelDelete', channel) | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -1,16 +1,19 @@ | |||
| import { Gateway, GatewayEventHandler } from '../index.ts' | ||||
| import cache from '../../models/cache.ts' | ||||
| import { TextChannel } from '../../structures/textChannel.ts' | ||||
| import { ChannelPayload } from "../../types/channelTypes.ts" | ||||
| 
 | ||||
| export const channelPinsUpdate: GatewayEventHandler = ( | ||||
|   gateway: Gateway, | ||||
|   d: any | ||||
| ) => { | ||||
|   const after: TextChannel = cache.get('textchannel', d.channel_id) | ||||
|   const after: TextChannel = gateway.client.channels.get(d.channel_id) | ||||
|   if (after !== undefined) { | ||||
|     const before = after.refreshFromData({ | ||||
|       last_pin_timestamp: d.last_pin_timestamp | ||||
|     }) | ||||
|     let raw = gateway.client.channels._get(d.channel_id) as ChannelPayload; | ||||
|     gateway.client.channels.set(after.id, Object.assign(raw, { last_pin_timestamp: d.last_pin_timestamp })) | ||||
|     gateway.client.emit('channelPinsUpdate', before, after) | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -1,4 +1,3 @@ | |||
| import cache from '../../models/cache.ts' | ||||
| import { Channel } from '../../structures/channel.ts' | ||||
| import getChannelByType from '../../utils/getChannelByType.ts' | ||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | ||||
|  | @ -7,9 +6,10 @@ export const channelUpdate: GatewayEventHandler = ( | |||
|   gateway: Gateway, | ||||
|   d: any | ||||
| ) => { | ||||
|   const oldChannel: Channel = cache.get('channel', d.id) | ||||
|   const oldChannel: Channel = gateway.client.channels.get(d.id) | ||||
| 
 | ||||
|   if (oldChannel !== undefined) { | ||||
|     gateway.client.channels.set(d.id, d) | ||||
|     if (oldChannel.type !== d.type) { | ||||
|       const channel: Channel = getChannelByType(gateway.client, d) ?? oldChannel | ||||
|       gateway.client.emit('channelUpdate', oldChannel, channel) | ||||
|  |  | |||
|  | @ -1,14 +1,15 @@ | |||
| import { Gateway, GatewayEventHandler } from '../index.ts' | ||||
| import cache from '../../models/cache.ts' | ||||
| import { Guild } from '../../structures/guild.ts' | ||||
| 
 | ||||
| export const guildCreate: GatewayEventHandler = (gateway: Gateway, d: any) => { | ||||
|   let guild: Guild = cache.get('guild', d.id) | ||||
|   let guild: Guild | void = gateway.client.guilds.get(d.id) | ||||
|   if (guild !== undefined) { | ||||
|     // It was just lazy load, so we don't fire the event as its gonna fire for every guild bot is in
 | ||||
|     gateway.client.guilds.set(d.id, d) | ||||
|     guild.refreshFromData(d) | ||||
|   } else { | ||||
|     guild = new Guild(gateway.client, d) | ||||
|     gateway.client.guilds.set(d.id, d) | ||||
|     guild = gateway.client.guilds.get(d.id) | ||||
|     gateway.client.emit('guildCreate', guild) | ||||
|   } | ||||
| 
 | ||||
|   gateway.client.emit('guildCreate', guild) | ||||
| } | ||||
|  |  | |||
|  | @ -1,13 +1,12 @@ | |||
| import cache from '../../models/cache.ts' | ||||
| import { Guild } from '../../structures/guild.ts' | ||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | ||||
| 
 | ||||
| export const guildDelte: GatewayEventHandler = (gateway: Gateway, d: any) => { | ||||
|   const guild: Guild = cache.get('guild', d.id) | ||||
|   const guild: Guild | void = gateway.client.guilds.get(d.id) | ||||
| 
 | ||||
|   if (guild !== undefined) { | ||||
|     guild.refreshFromData(d) | ||||
|     cache.del('guild', d.id) | ||||
|     gateway.client.guilds.delete(d.id) | ||||
|     gateway.client.emit('guildDelete', guild) | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -3,9 +3,9 @@ import cache from '../../models/cache.ts' | |||
| import { Guild } from '../../structures/guild.ts' | ||||
| 
 | ||||
| export const guildUpdate: GatewayEventHandler = (gateway: Gateway, d: any) => { | ||||
|   const after: Guild = cache.get('guild', d.id) | ||||
|   if (after !== undefined) { | ||||
|     const before: Guild = after.refreshFromData(d) | ||||
|     gateway.client.emit('guildUpdate', before, after) | ||||
|   } | ||||
|   const before: Guild | void = gateway.client.guilds.get(d.id) | ||||
|   if(!before) return | ||||
|   gateway.client.guilds.set(d.id, d) | ||||
|   const after: Guild | void = gateway.client.guilds.get(d.id) | ||||
|   gateway.client.emit('guildUpdate', before, after) | ||||
| } | ||||
|  |  | |||
|  | @ -1,4 +1,3 @@ | |||
| import { Guild } from '../../structures/guild.ts' | ||||
| import { User } from '../../structures/user.ts' | ||||
| import { GuildPayload } from '../../types/guildTypes.ts' | ||||
| import { Gateway, GatewayEventHandler } from '../index.ts' | ||||
|  | @ -6,6 +5,9 @@ import { Gateway, GatewayEventHandler } from '../index.ts' | |||
| export const ready: GatewayEventHandler = (gateway: Gateway, d: any) => { | ||||
|   gateway.client.user = new User(gateway.client, d.user) | ||||
|   gateway.sessionID = d.session_id | ||||
|   d.guilds.forEach((guild: GuildPayload) => new Guild(gateway.client, guild)) | ||||
|   gateway.debug(`Received READY. Session: ${gateway.sessionID}`) | ||||
|   d.guilds.forEach((guild: GuildPayload) => { | ||||
|     gateway.client.guilds.set(guild.id, guild) | ||||
|   }) | ||||
|   gateway.client.emit('ready') | ||||
| } | ||||
|  | @ -7,6 +7,8 @@ import { | |||
| import { GatewayResponse } from '../types/gatewayResponse.ts' | ||||
| import { GatewayOpcodes, GatewayIntents } from '../types/gatewayTypes.ts' | ||||
| import { gatewayHandlers } from './handlers/index.ts' | ||||
| import { GATEWAY_BOT } from '../types/endpoint.ts' | ||||
| import { GatewayBotPayload } from "../types/gatewayBot.ts" | ||||
| 
 | ||||
| /** | ||||
|  * Handles Discord gateway connection. | ||||
|  | @ -24,7 +26,7 @@ class Gateway { | |||
|   heartbeatIntervalID?: number | ||||
|   sequenceID?: number | ||||
|   sessionID?: string | ||||
|   lastPingTimestemp = 0 | ||||
|   lastPingTimestamp = 0 | ||||
|   private heartbeatServerResponded = false | ||||
|   client: Client | ||||
| 
 | ||||
|  | @ -46,6 +48,7 @@ class Gateway { | |||
| 
 | ||||
|   private onopen (): void { | ||||
|     this.connected = true | ||||
|     this.debug("Connected to Gateway!") | ||||
|   } | ||||
| 
 | ||||
|   private onmessage (event: MessageEvent): void { | ||||
|  | @ -63,6 +66,7 @@ class Gateway { | |||
|     switch (op) { | ||||
|       case GatewayOpcodes.HELLO: | ||||
|         this.heartbeatInterval = d.heartbeat_interval | ||||
|         this.debug(`Received HELLO. Heartbeat Interval: ${this.heartbeatInterval}`) | ||||
|         this.heartbeatIntervalID = setInterval(() => { | ||||
|           if (this.heartbeatServerResponded) { | ||||
|             this.heartbeatServerResponded = false | ||||
|  | @ -79,7 +83,7 @@ class Gateway { | |||
|               d: this.sequenceID ?? null | ||||
|             }) | ||||
|           ) | ||||
|           this.lastPingTimestemp = Date.now() | ||||
|           this.lastPingTimestamp = Date.now() | ||||
|         }, this.heartbeatInterval) | ||||
| 
 | ||||
|         if (!this.initialized) { | ||||
|  | @ -92,7 +96,8 @@ class Gateway { | |||
| 
 | ||||
|       case GatewayOpcodes.HEARTBEAT_ACK: | ||||
|         this.heartbeatServerResponded = true | ||||
|         this.client.ping = Date.now() - this.lastPingTimestemp | ||||
|         this.client.ping = Date.now() - this.lastPingTimestamp | ||||
|         this.debug(`Received Heartbeat Ack. Ping Recognized: ${this.client.ping}ms`) | ||||
|         break | ||||
| 
 | ||||
|       case GatewayOpcodes.INVALID_SESSION: | ||||
|  | @ -135,7 +140,14 @@ class Gateway { | |||
|     console.log(eventError) | ||||
|   } | ||||
| 
 | ||||
|   private sendIdentify (): void { | ||||
|   private async sendIdentify () { | ||||
|     this.debug("Fetching /gateway/bot...") | ||||
|     let info = await this.client.rest.get(GATEWAY_BOT()) as GatewayBotPayload | ||||
|     if(info.session_start_limit.remaining == 0) throw new Error("Session Limit Reached. Retry After " + info.session_start_limit.reset_after + "ms") | ||||
|     this.debug("Recommended Shards: " + info.shards) | ||||
|     this.debug("=== Session Limit Info ===") | ||||
|     this.debug(`Remaining: ${info.session_start_limit.remaining}/${info.session_start_limit.total}`) | ||||
|     this.debug(`Reset After: ${info.session_start_limit.reset_after}ms`) | ||||
|     this.websocket.send( | ||||
|       JSON.stringify({ | ||||
|         op: GatewayOpcodes.IDENTIFY, | ||||
|  | @ -164,6 +176,7 @@ class Gateway { | |||
|   } | ||||
| 
 | ||||
|   private sendResume (): void { | ||||
|     this.debug(`Preparing to resume with Session: ${this.sessionID}`) | ||||
|     this.websocket.send( | ||||
|       JSON.stringify({ | ||||
|         op: GatewayOpcodes.RESUME, | ||||
|  | @ -176,6 +189,10 @@ class Gateway { | |||
|     ) | ||||
|   } | ||||
| 
 | ||||
|   debug(msg: string) { | ||||
|     this.client.debug("Gateway", msg) | ||||
|   } | ||||
| 
 | ||||
|   initWebsocket (): void { | ||||
|     this.websocket = new WebSocket( | ||||
|       // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
 | ||||
|  |  | |||
							
								
								
									
										32
									
								
								src/managers/BaseManager.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/managers/BaseManager.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,32 @@ | |||
| import { Client } from "../models/client.ts"; | ||||
| import { Base } from "../structures/base.ts"; | ||||
| 
 | ||||
| export class BaseManager<T, T2> { | ||||
|   client: Client | ||||
|   cacheName: string | ||||
|   dataType: typeof Base | ||||
| 
 | ||||
|   constructor(client: Client, cacheName: string, dataType: typeof Base) { | ||||
|     this.client = client | ||||
|     this.cacheName = cacheName | ||||
|     this.dataType = dataType | ||||
|   } | ||||
| 
 | ||||
|   _get(key: string): T { | ||||
|     return this.client.cache.get(this.cacheName, key) as T | ||||
|   } | ||||
| 
 | ||||
|   get(key: string): T2 | void { | ||||
|     let raw = this._get(key) | ||||
|     if(!raw) return | ||||
|     return new this.dataType(this.client, raw) as any | ||||
|   } | ||||
| 
 | ||||
|   set(key: string, value: T) { | ||||
|     return this.client.cache.set(this.cacheName, key, value) | ||||
|   } | ||||
| 
 | ||||
|   delete(key: string) { | ||||
|     return this.client.cache.delete(this.cacheName, key) | ||||
|   } | ||||
| } | ||||
							
								
								
									
										26
									
								
								src/managers/ChannelsManager.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/managers/ChannelsManager.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,26 @@ | |||
| import { Client } from "../models/client.ts"; | ||||
| import { Channel } from "../structures/channel.ts"; | ||||
| import { User } from "../structures/user.ts"; | ||||
| import { ChannelPayload } from "../types/channelTypes.ts"; | ||||
| import { CHANNEL } from "../types/endpoint.ts"; | ||||
| import { BaseManager } from "./BaseManager.ts"; | ||||
| 
 | ||||
| export class ChannelsManager extends BaseManager<ChannelPayload, Channel> { | ||||
|   constructor(client: Client) { | ||||
|     super(client, "channels", User) | ||||
|   } | ||||
| 
 | ||||
|   // Override get method as Generic
 | ||||
|   get<T = Channel>(key: string): T { | ||||
|     return new this.dataType(this.client, this._get(key)) as any | ||||
|   } | ||||
| 
 | ||||
|   fetch(id: string) { | ||||
|     return new Promise((res, rej) => { | ||||
|       this.client.rest.get(CHANNEL(id)).then(data => { | ||||
|         this.set(id, data as ChannelPayload) | ||||
|         res(new Channel(this.client, data as ChannelPayload)) | ||||
|       }).catch(e => rej(e)) | ||||
|     }) | ||||
|   } | ||||
| } | ||||
							
								
								
									
										20
									
								
								src/managers/EmojisManager.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/managers/EmojisManager.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | |||
| import { Client } from "../models/client.ts"; | ||||
| import { Emoji } from "../structures/emoji.ts"; | ||||
| import { EmojiPayload } from "../types/emojiTypes.ts"; | ||||
| import { CHANNEL } from "../types/endpoint.ts"; | ||||
| import { BaseManager } from "./BaseManager.ts"; | ||||
| 
 | ||||
| export class EmojisManager extends BaseManager<EmojiPayload, Emoji> { | ||||
|   constructor(client: Client) { | ||||
|     super(client, "emojis", Emoji) | ||||
|   } | ||||
| 
 | ||||
|   fetch(id: string) { | ||||
|     return new Promise((res, rej) => { | ||||
|       this.client.rest.get(CHANNEL(id)).then(data => { | ||||
|         this.set(id, data as EmojiPayload) | ||||
|         res(new Emoji(this.client, data as EmojiPayload)) | ||||
|       }).catch(e => rej(e)) | ||||
|     }) | ||||
|   } | ||||
| } | ||||
							
								
								
									
										20
									
								
								src/managers/GuildsManager.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/managers/GuildsManager.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | |||
| import { Client } from "../models/client.ts"; | ||||
| import { Guild } from "../structures/guild.ts"; | ||||
| import { GUILD } from "../types/endpoint.ts"; | ||||
| import { GuildPayload } from "../types/guildTypes.ts"; | ||||
| import { BaseManager } from "./BaseManager.ts"; | ||||
| 
 | ||||
| export class GuildManager extends BaseManager<GuildPayload, Guild> { | ||||
|   constructor(client: Client) { | ||||
|     super(client, "guilds", Guild) | ||||
|   } | ||||
| 
 | ||||
|   fetch(id: string) { | ||||
|     return new Promise((res, rej) => { | ||||
|       this.client.rest.get(GUILD(id)).then(data => { | ||||
|         this.set(id, data as GuildPayload) | ||||
|         res(new Guild(this.client, data as GuildPayload)) | ||||
|       }).catch(e => rej(e)) | ||||
|     }) | ||||
|   } | ||||
| } | ||||
							
								
								
									
										25
									
								
								src/managers/RolesManager.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/managers/RolesManager.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,25 @@ | |||
| import { Client } from "../models/client.ts"; | ||||
| import { Guild } from "../structures/guild.ts"; | ||||
| import { Role } from "../structures/role.ts"; | ||||
| import { User } from "../structures/user.ts"; | ||||
| import { GUILD_ROLE } from "../types/endpoint.ts"; | ||||
| import { RolePayload } from "../types/roleTypes.ts"; | ||||
| import { BaseManager } from "./BaseManager.ts"; | ||||
| 
 | ||||
| export class RolesManager extends BaseManager<RolePayload, Role> { | ||||
|   guild: Guild | ||||
| 
 | ||||
|   constructor(client: Client, guild: Guild) { | ||||
|     super(client, "roles:" + guild.id, Role) | ||||
|     this.guild = guild | ||||
|   } | ||||
| 
 | ||||
|   fetch(id: string) { | ||||
|     return new Promise((res, rej) => { | ||||
|       this.client.rest.get(GUILD_ROLE(this.guild.id, id)).then(data => { | ||||
|         this.set(id, data as RolePayload) | ||||
|         res(new Role(this.client, data as RolePayload)) | ||||
|       }).catch(e => rej(e)) | ||||
|     }) | ||||
|   } | ||||
| } | ||||
							
								
								
									
										20
									
								
								src/managers/UsersManager.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/managers/UsersManager.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | |||
| import { Client } from "../models/client.ts"; | ||||
| import { User } from "../structures/user.ts"; | ||||
| import { USER } from "../types/endpoint.ts"; | ||||
| import { UserPayload } from "../types/userTypes.ts"; | ||||
| import { BaseManager } from "./BaseManager.ts"; | ||||
| 
 | ||||
| export class UserManager extends BaseManager<UserPayload, User> { | ||||
|   constructor(client: Client) { | ||||
|     super(client, "users", User) | ||||
|   } | ||||
| 
 | ||||
|   fetch(id: string) { | ||||
|     return new Promise((res, rej) => { | ||||
|       this.client.rest.get(USER(id)).then(data => { | ||||
|         this.set(id, data as UserPayload) | ||||
|         res(new User(this.client, data as UserPayload)) | ||||
|       }).catch(e => rej(e)) | ||||
|     }) | ||||
|   } | ||||
| } | ||||
							
								
								
									
										48
									
								
								src/models/CacheAdapter.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/models/CacheAdapter.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,48 @@ | |||
| import { Collection } from "../utils/collection.ts"; | ||||
| import { Client } from "./client.ts"; | ||||
| 
 | ||||
| export interface ICacheAdapter { | ||||
|   client: Client | ||||
|   get: (cacheName: string, key: string) => any | ||||
|   set: (cacheName: string, key: string, value: any) => any | ||||
|   delete: (cacheName: string, key: string) => boolean | ||||
|   array: (cacheName: string) => void | any[] | ||||
| } | ||||
| 
 | ||||
| export class DefaultCacheAdapter implements ICacheAdapter { | ||||
|   client: Client | ||||
|   data: { | ||||
|     [name: string]: Collection<string, any> | ||||
|   } = {} | ||||
| 
 | ||||
|   constructor(client: Client) { | ||||
|     this.client = client | ||||
|   } | ||||
| 
 | ||||
|   get(cacheName: string, key: string) { | ||||
|     let cache = this.data[cacheName] | ||||
|     if (!cache) return; | ||||
|     return cache.get(key) | ||||
|   } | ||||
| 
 | ||||
|   set(cacheName: string, key: string, value: any) { | ||||
|     let cache = this.data[cacheName] | ||||
|     if (!cache) { | ||||
|       this.data[cacheName] = new Collection() | ||||
|       cache = this.data[cacheName] | ||||
|     } | ||||
|     cache.set(key, value) | ||||
|   } | ||||
| 
 | ||||
|   delete(cacheName: string, key: string) { | ||||
|     let cache = this.data[cacheName] | ||||
|     if (!cache) return false | ||||
|     return cache.delete(key) | ||||
|   } | ||||
| 
 | ||||
|   array(cacheName: string) { | ||||
|     let cache = this.data[cacheName] | ||||
|     if (!cache) return | ||||
|     return cache.array() | ||||
|   } | ||||
| } | ||||
|  | @ -1,30 +1,64 @@ | |||
| import { User } from '../structures/user.ts' | ||||
| import { GatewayIntents } from '../types/gatewayTypes.ts' | ||||
| import { Gateway } from '../gateway/index.ts' | ||||
| import { Rest } from './rest.ts' | ||||
| import { RESTManager } from './rest.ts' | ||||
| import EventEmitter from 'https://deno.land/std@0.74.0/node/events.ts' | ||||
| import { DefaultCacheAdapter, ICacheAdapter } from "./CacheAdapter.ts" | ||||
| import { UserManager } from "../managers/UsersManager.ts" | ||||
| import { GuildManager } from "../managers/GuildsManager.ts" | ||||
| import { EmojisManager } from "../managers/EmojisManager.ts" | ||||
| import { ChannelsManager } from "../managers/ChannelsManager.ts" | ||||
| 
 | ||||
| /** Some Client Options to modify behaviour */ | ||||
| export interface ClientOptions { | ||||
|   token?: string | ||||
|   intents?: GatewayIntents[] | ||||
|   cache?: ICacheAdapter | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Discord Client. | ||||
|  */ | ||||
| export class Client extends EventEmitter { | ||||
|   gateway?: Gateway | ||||
|   rest?: Rest | ||||
|   rest: RESTManager = new RESTManager(this) | ||||
|   user?: User | ||||
|   ping = 0 | ||||
|   token?: string | ||||
|   cache: ICacheAdapter = new DefaultCacheAdapter(this) | ||||
|   intents?: GatewayIntents[] | ||||
|   users: UserManager = new UserManager(this) | ||||
|   guilds: GuildManager = new GuildManager(this) | ||||
|   channels: ChannelsManager = new ChannelsManager(this) | ||||
|   emojis: EmojisManager = new EmojisManager(this) | ||||
| 
 | ||||
|   // constructor () {
 | ||||
|   //   super()
 | ||||
|   // }
 | ||||
|   constructor (options: ClientOptions = {}) { | ||||
|     super() | ||||
|     this.token = options.token | ||||
|     this.intents = options.intents | ||||
|     if(options.cache) this.cache = options.cache | ||||
|   } | ||||
| 
 | ||||
|   debug(tag: string, msg: string) { | ||||
|     this.emit("debug", `[${tag}] ${msg}`) | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * This function is used for connect to discord. | ||||
|    * @param token Your token. This is required. | ||||
|    * @param intents Gateway intents in array. This is required. | ||||
|    */ | ||||
|   connect (token: string, intents: GatewayIntents[]): void { | ||||
|     this.token = token | ||||
|   connect (token?: string, intents?: GatewayIntents[]): void { | ||||
|     if(!token && this.token) token = this.token | ||||
|     else if(!this.token && token) { | ||||
|       this.token = token | ||||
|     } | ||||
|     else throw new Error("No Token Provided") | ||||
|     if(!intents && this.intents) intents = this.intents | ||||
|     else if(intents && !this.intents) { | ||||
|       this.intents = intents | ||||
|     } | ||||
|     else throw new Error("No Gateway Intents were provided") | ||||
|     this.gateway = new Gateway(this, token, intents) | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -1,11 +1,364 @@ | |||
| import { Client } from './client.ts' | ||||
| import { delay } from "../utils/index.ts"; | ||||
| import * as baseEndpoints from "../consts/urlsAndVersions.ts"; | ||||
| import { Client } from "./client.ts"; | ||||
| 
 | ||||
| class Rest { | ||||
|   client: Client | ||||
|   constructor (client: Client) { | ||||
|     this.client = client | ||||
|   } | ||||
|   // TODO: make endpoints function
 | ||||
| export enum HttpResponseCode { | ||||
|   Ok = 200, | ||||
|   Created = 201, | ||||
|   NoContent = 204, | ||||
|   NotModified = 304, | ||||
|   BadRequest = 400, | ||||
|   Unauthorized = 401, | ||||
|   Forbidden = 403, | ||||
|   NotFound = 404, | ||||
|   MethodNotAllowed = 405, | ||||
|   TooManyRequests = 429, | ||||
|   GatewayUnavailable = 502, | ||||
|   // ServerError left untyped because it's 5xx.
 | ||||
| } | ||||
| 
 | ||||
| export { Rest } | ||||
| export type RequestMethods = | ||||
|   | "get" | ||||
|   | "post" | ||||
|   | "put" | ||||
|   | "patch" | ||||
|   | "head" | ||||
|   | "delete"; | ||||
| 
 | ||||
| export interface QueuedRequest { | ||||
| 	callback: () => Promise< | ||||
| 		void | { | ||||
| 			rateLimited: any; | ||||
| 			beforeFetch: boolean; | ||||
| 			bucketID?: string | null; | ||||
| 		} | ||||
| 	>; | ||||
| 	bucketID?: string | null; | ||||
| 	url: string; | ||||
| } | ||||
| 
 | ||||
| export interface RateLimitedPath { | ||||
| 	url: string; | ||||
| 	resetTimestamp: number; | ||||
| 	bucketID: string | null; | ||||
| } | ||||
| 
 | ||||
| export class RESTManager { | ||||
| 	client: Client; | ||||
| 	globallyRateLimited: boolean = false; | ||||
| 	queueInProcess: boolean = false; | ||||
| 	pathQueues: { [key: string]: QueuedRequest[] } = {}; | ||||
| 	ratelimitedPaths = new Map<string, RateLimitedPath>(); | ||||
| 
 | ||||
| 	constructor(client: Client) { | ||||
| 		this.client = client; | ||||
| 	} | ||||
| 
 | ||||
| 	async processRateLimitedPaths() { | ||||
| 		const now = Date.now(); | ||||
| 		this.ratelimitedPaths.forEach((value, key) => { | ||||
| 			if (value.resetTimestamp > now) return; | ||||
| 			this.ratelimitedPaths.delete(key); | ||||
| 			if (key === "global") this.globallyRateLimited = false; | ||||
| 		}); | ||||
| 
 | ||||
| 		await delay(1000); | ||||
| 		this.processRateLimitedPaths(); | ||||
| 	} | ||||
| 
 | ||||
| 	addToQueue(request: QueuedRequest) { | ||||
| 		const route = request.url.substring(baseEndpoints.DISCORD_API_URL.length + 1); | ||||
| 		const parts = route.split("/"); | ||||
| 		// Remove the major param
 | ||||
| 		parts.shift(); | ||||
| 		const [id] = parts; | ||||
| 	 | ||||
| 		if (this.pathQueues[id]) { | ||||
| 			this.pathQueues[id].push(request); | ||||
| 		} else { | ||||
| 			this.pathQueues[id] = [request]; | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	async cleanupQueues() { | ||||
| 		Object.entries(this.pathQueues).map(([key, value]) => { | ||||
| 			if (!value.length) { | ||||
| 				// Remove it entirely
 | ||||
| 				delete this.pathQueues[key]; | ||||
| 			} | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	async processQueue() { | ||||
| 		if ( | ||||
| 			(Object.keys(this.pathQueues).length) && !this.globallyRateLimited | ||||
| 		) { | ||||
| 			await Promise.allSettled( | ||||
| 				Object.values(this.pathQueues).map(async (pathQueue) => { | ||||
| 					const request = pathQueue.shift(); | ||||
| 					if (!request) return; | ||||
| 	 | ||||
| 					const rateLimitedURLResetIn = await this.checkRatelimits(request.url); | ||||
| 	 | ||||
| 					if (request.bucketID) { | ||||
| 						const rateLimitResetIn = await this.checkRatelimits(request.bucketID); | ||||
| 						if (rateLimitResetIn) { | ||||
| 							// This request is still rate limited readd to queue
 | ||||
| 							this.addToQueue(request); | ||||
| 						} else if (rateLimitedURLResetIn) { | ||||
| 							// This URL is rate limited readd to queue
 | ||||
| 							this.addToQueue(request); | ||||
| 						} else { | ||||
| 							// This request is not rate limited so it should be run
 | ||||
| 							const result = await request.callback(); | ||||
| 							if (result && result.rateLimited) { | ||||
| 								this.addToQueue( | ||||
| 									{ ...request, bucketID: result.bucketID || request.bucketID }, | ||||
| 								); | ||||
| 							} | ||||
| 						} | ||||
| 					} else { | ||||
| 						if (rateLimitedURLResetIn) { | ||||
| 							// This URL is rate limited readd to queue
 | ||||
| 							this.addToQueue(request); | ||||
| 						} else { | ||||
| 							// This request has no bucket id so it should be processed
 | ||||
| 							const result = await request.callback(); | ||||
| 							if (request && result && result.rateLimited) { | ||||
| 								this.addToQueue( | ||||
| 									{ ...request, bucketID: result.bucketID || request.bucketID }, | ||||
| 								); | ||||
| 							} | ||||
| 						} | ||||
| 					} | ||||
| 				}), | ||||
| 			); | ||||
| 		} | ||||
| 	 | ||||
| 		if (Object.keys(this.pathQueues).length) { | ||||
| 			await delay(1000); | ||||
| 			this.processQueue(); | ||||
| 			this.cleanupQueues(); | ||||
| 		} else this.queueInProcess = false; | ||||
| 	} | ||||
| 
 | ||||
| 	createRequestBody(body: any, method: RequestMethods) { | ||||
| 		const headers: { [key: string]: string } = { | ||||
| 			Authorization: `Bot ${this.client.token}`, | ||||
| 			"User-Agent": | ||||
| 				`DiscordBot (discord.deno)`, | ||||
| 		}; | ||||
| 
 | ||||
| 		if(!this.client.token) delete headers["Authorization"]; | ||||
| 	 | ||||
| 		if (method === "get") body = undefined; | ||||
| 	 | ||||
| 		if (body?.reason) { | ||||
| 			headers["X-Audit-Log-Reason"] = encodeURIComponent(body.reason); | ||||
| 		} | ||||
| 	 | ||||
| 		if (body?.file) { | ||||
| 			const form = new FormData(); | ||||
| 			form.append("file", body.file.blob, body.file.name); | ||||
| 			form.append("payload_json", JSON.stringify({ ...body, file: undefined })); | ||||
| 			body.file = form; | ||||
| 		} else if ( | ||||
| 			body && !["get", "delete"].includes(method) | ||||
| 		) { | ||||
| 			headers["Content-Type"] = "application/json"; | ||||
| 		} | ||||
| 	 | ||||
| 		return { | ||||
| 			headers, | ||||
| 			body: body?.file || JSON.stringify(body), | ||||
| 			method: method.toUpperCase(), | ||||
| 		}; | ||||
| 	} | ||||
| 
 | ||||
| 	async checkRatelimits(url: string) { | ||||
| 		const ratelimited = this.ratelimitedPaths.get(url); | ||||
| 		const global = this.ratelimitedPaths.get("global"); | ||||
| 		const now = Date.now(); | ||||
| 	 | ||||
| 		if (ratelimited && now < ratelimited.resetTimestamp) { | ||||
| 			return ratelimited.resetTimestamp - now; | ||||
| 		} | ||||
| 		if (global && now < global.resetTimestamp) { | ||||
| 			return global.resetTimestamp - now; | ||||
| 		} | ||||
| 	 | ||||
| 		return false; | ||||
| 	} | ||||
| 
 | ||||
| 	async runMethod( | ||||
| 		method: RequestMethods, | ||||
| 		url: string, | ||||
| 		body?: unknown, | ||||
| 		retryCount = 0, | ||||
| 		bucketID?: string | null, | ||||
| 	) { | ||||
| 	 | ||||
| 		const errorStack = new Error("Location In Your Files:"); | ||||
| 		Error.captureStackTrace(errorStack); | ||||
| 	 | ||||
| 		return new Promise((resolve, reject) => { | ||||
| 			const callback = async () => { | ||||
| 				try { | ||||
| 					const rateLimitResetIn = await this.checkRatelimits(url); | ||||
| 					if (rateLimitResetIn) { | ||||
| 						return { rateLimited: rateLimitResetIn, beforeFetch: true, bucketID }; | ||||
| 					} | ||||
| 	 | ||||
| 					const query = method === "get" && body | ||||
| 						? Object.entries(body as any).map(([key, value]) => | ||||
| 							`${encodeURIComponent(key)}=${encodeURIComponent(value as any)}` | ||||
| 						) | ||||
| 							.join("&") | ||||
| 						: ""; | ||||
| 					const urlToUse = method === "get" && query ? `${url}?${query}` : url; | ||||
| 
 | ||||
| 					const response = await fetch(urlToUse, this.createRequestBody(body, method)); | ||||
| 					const bucketIDFromHeaders = this.processHeaders(url, response.headers); | ||||
| 					this.handleStatusCode(response, errorStack); | ||||
| 	 | ||||
| 					// Sometimes Discord returns an empty 204 response that can't be made to JSON.
 | ||||
| 					if (response.status === 204) return resolve(); | ||||
| 	 | ||||
| 					const json = await response.json(); | ||||
| 					if ( | ||||
| 						json.retry_after || | ||||
| 						json.message === "You are being rate limited." | ||||
| 					) { | ||||
| 						if (retryCount > 10) { | ||||
| 							throw new Error("Max RateLimit Retries hit"); | ||||
| 						} | ||||
| 	 | ||||
| 						return { | ||||
| 							rateLimited: json.retry_after, | ||||
| 							beforeFetch: false, | ||||
| 							bucketID: bucketIDFromHeaders, | ||||
| 						}; | ||||
| 					} | ||||
| 					return resolve(json); | ||||
| 				} catch (error) { | ||||
| 					return reject(error); | ||||
| 				} | ||||
| 			}; | ||||
| 	 | ||||
| 			this.addToQueue({ | ||||
| 				callback, | ||||
| 				bucketID, | ||||
| 				url, | ||||
| 			}); | ||||
| 			if (!this.queueInProcess) { | ||||
| 				this.queueInProcess = true; | ||||
| 				this.processQueue(); | ||||
| 			} | ||||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	async logErrors(response: Response, errorStack?: unknown) { | ||||
| 		try { | ||||
| 			const error = await response.json(); | ||||
| 			console.error(error); | ||||
| 		} catch { | ||||
| 			console.error(response); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	handleStatusCode(response: Response, errorStack?: unknown) { | ||||
| 		const status = response.status; | ||||
| 	 | ||||
| 		if ( | ||||
| 			(status >= 200 && status < 400) || | ||||
| 			status === HttpResponseCode.TooManyRequests | ||||
| 		) { | ||||
| 			return true; | ||||
| 		} | ||||
| 	 | ||||
| 		this.logErrors(response, errorStack); | ||||
| 	 | ||||
| 		switch (status) { | ||||
| 			case HttpResponseCode.BadRequest: | ||||
| 			case HttpResponseCode.Unauthorized: | ||||
| 			case HttpResponseCode.Forbidden: | ||||
| 			case HttpResponseCode.NotFound: | ||||
| 			case HttpResponseCode.MethodNotAllowed: | ||||
| 				throw new Error("Request Client Error"); | ||||
| 			case HttpResponseCode.GatewayUnavailable: | ||||
| 				throw new Error("Request Server Error"); | ||||
| 		} | ||||
| 	 | ||||
| 		// left are all unknown
 | ||||
| 		throw new Error("Request Unknown Error"); | ||||
| 	} | ||||
| 
 | ||||
| 	processHeaders(url: string, headers: Headers) { | ||||
| 		let ratelimited = false; | ||||
| 	 | ||||
| 		// Get all useful headers
 | ||||
| 		const remaining = headers.get("x-ratelimit-remaining"); | ||||
| 		const resetTimestamp = headers.get("x-ratelimit-reset"); | ||||
| 		const retryAfter = headers.get("retry-after"); | ||||
| 		const global = headers.get("x-ratelimit-global"); | ||||
| 		const bucketID = headers.get("x-ratelimit-bucket"); | ||||
| 	 | ||||
| 		// If there is no remaining rate limit for this endpoint, we save it in cache
 | ||||
| 		if (remaining && remaining === "0") { | ||||
| 			ratelimited = true; | ||||
| 	 | ||||
| 			this.ratelimitedPaths.set(url, { | ||||
| 				url, | ||||
| 				resetTimestamp: Number(resetTimestamp) * 1000, | ||||
| 				bucketID, | ||||
| 			}); | ||||
| 	 | ||||
| 			if (bucketID) { | ||||
| 				this.ratelimitedPaths.set(bucketID, { | ||||
| 					url, | ||||
| 					resetTimestamp: Number(resetTimestamp) * 1000, | ||||
| 					bucketID, | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
| 	 | ||||
| 		// If there is no remaining global limit, we save it in cache
 | ||||
| 		if (global) { | ||||
| 			const reset = Date.now() + Number(retryAfter); | ||||
| 			this.globallyRateLimited = true; | ||||
| 			ratelimited = true; | ||||
| 	 | ||||
| 			this.ratelimitedPaths.set("global", { | ||||
| 				url: "global", | ||||
| 				resetTimestamp: reset, | ||||
| 				bucketID, | ||||
| 			}); | ||||
| 	 | ||||
| 			if (bucketID) { | ||||
| 				this.ratelimitedPaths.set(bucketID, { | ||||
| 					url: "global", | ||||
| 					resetTimestamp: reset, | ||||
| 					bucketID, | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
| 	 | ||||
| 		return ratelimited ? bucketID : undefined; | ||||
| 	} | ||||
| 
 | ||||
| 	get(url: string, body?: unknown) { | ||||
|     return this.runMethod("get", url, body); | ||||
|   } | ||||
|   post(url: string, body?: unknown) { | ||||
|     return this.runMethod("post", url, body); | ||||
|   } | ||||
|   delete(url: string, body?: unknown) { | ||||
|     return this.runMethod("delete", url, body); | ||||
|   } | ||||
|   patch(url: string, body?: unknown) { | ||||
|     return this.runMethod("patch", url, body); | ||||
|   } | ||||
|   put(url: string, body?: unknown) { | ||||
|     return this.runMethod("put", url, body); | ||||
|   } | ||||
| } | ||||
|  | @ -5,10 +5,11 @@ import { Base } from './base.ts' | |||
| import { Channel } from './channel.ts' | ||||
| import { Emoji } from './emoji.ts' | ||||
| import { Member } from './member.ts' | ||||
| import { Role } from './role.ts' | ||||
| import { VoiceState } from './voiceState.ts' | ||||
| import cache from '../models/cache.ts' | ||||
| import getChannelByType from '../utils/getChannelByType.ts' | ||||
| import { RolesManager } from "../managers/RolesManager.ts" | ||||
| import { Role } from "./role.ts" | ||||
| 
 | ||||
| export class Guild extends Base { | ||||
|   id: string | ||||
|  | @ -28,7 +29,7 @@ export class Guild extends Base { | |||
|   verificationLevel?: string | ||||
|   defaultMessageNotifications?: string | ||||
|   explicitContentFilter?: string | ||||
|   roles?: Role[] | ||||
|   roles: RolesManager = new RolesManager(this.client, this) | ||||
|   emojis?: Emoji[] | ||||
|   features?: GuildFeatures[] | ||||
|   mfaLevel?: string | ||||
|  | @ -79,9 +80,12 @@ export class Guild extends Base { | |||
|       this.verificationLevel = data.verification_level | ||||
|       this.defaultMessageNotifications = data.default_message_notifications | ||||
|       this.explicitContentFilter = data.explicit_content_filter | ||||
|       this.roles = data.roles.map( | ||||
|         v => cache.get('role', v.id) ?? new Role(client, v) | ||||
|       ) | ||||
|       // this.roles = data.roles.map(
 | ||||
|       //   v => cache.get('role', v.id) ?? new Role(client, v)
 | ||||
|       // )
 | ||||
|       data.roles.forEach(role => { | ||||
|         this.roles.set(role.id, new Role(client, role)) | ||||
|       }) | ||||
|       this.emojis = data.emojis.map( | ||||
|         v => cache.get('emoji', v.id) ?? new Emoji(client, v) | ||||
|       ) | ||||
|  | @ -120,7 +124,6 @@ export class Guild extends Base { | |||
|       this.approximateNumberCount = data.approximate_number_count | ||||
|       this.approximatePresenceCount = data.approximate_presence_count | ||||
|     } | ||||
|     cache.set('guild', this.id, this) | ||||
|   } | ||||
| 
 | ||||
|   protected readFromData (data: GuildPayload): void { | ||||
|  | @ -147,10 +150,10 @@ export class Guild extends Base { | |||
|         data.default_message_notifications ?? this.defaultMessageNotifications | ||||
|       this.explicitContentFilter = | ||||
|         data.explicit_content_filter ?? this.explicitContentFilter | ||||
|       this.roles = | ||||
|         data.roles.map( | ||||
|           v => cache.get('role', v.id) ?? new Role(this.client, v) | ||||
|         ) ?? this.roles | ||||
|       // this.roles =
 | ||||
|       //   data.roles.map(
 | ||||
|       //     v => cache.get('role', v.id) ?? new Role(this.client, v)
 | ||||
|       //   ) ?? this.roles
 | ||||
|       this.emojis = | ||||
|         data.emojis.map( | ||||
|           v => cache.get('emoji', v.id) ?? new Emoji(this.client, v) | ||||
|  |  | |||
|  | @ -18,6 +18,10 @@ export class User extends Base { | |||
|   premiumType?: 0 | 1 | 2 | ||||
|   publicFlags?: number | ||||
| 
 | ||||
|   get tag(): string { | ||||
|     return `${this.username}#${this.discriminator}`; | ||||
|   } | ||||
| 
 | ||||
|   get nickMention (): string { | ||||
|     return `<@!${this.id}>` | ||||
|   } | ||||
|  | @ -59,4 +63,8 @@ export class User extends Base { | |||
|     this.premiumType = data.premium_type ?? this.premiumType | ||||
|     this.publicFlags = data.public_flags ?? this.publicFlags | ||||
|   } | ||||
| 
 | ||||
|   toString() { | ||||
|     return this.mention; | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -1,2 +1 @@ | |||
| const TOKEN = '' | ||||
| export { TOKEN } | ||||
| export const TOKEN = '' | ||||
|  | @ -10,9 +10,11 @@ import { User } from '../structures/user.ts' | |||
| const bot = new Client() | ||||
| 
 | ||||
| bot.on('ready', () => { | ||||
|   console.log('READY!') | ||||
|   console.log(`[Login] Logged in as ${bot.user?.tag}!`) | ||||
| }) | ||||
| 
 | ||||
| bot.on('debug', console.log) | ||||
| 
 | ||||
| bot.on('channelDelete', (channel: Channel) => { | ||||
|   console.log('channelDelete', channel.id) | ||||
| }) | ||||
|  |  | |||
							
								
								
									
										11
									
								
								src/types/gatewayBot.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/types/gatewayBot.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | |||
| export interface ISessionStartLimit { | ||||
|   total: number | ||||
|   remaining: number | ||||
|   reset_after: number | ||||
| } | ||||
| 
 | ||||
| export interface GatewayBotPayload { | ||||
|   url: string | ||||
|   shards: number | ||||
|   session_start_limit: ISessionStartLimit | ||||
| } | ||||
							
								
								
									
										87
									
								
								src/utils/collection.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/utils/collection.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,87 @@ | |||
| export class Collection<K, V> extends Map<K, V> { | ||||
|   maxSize?: number; | ||||
| 
 | ||||
|   set(key: K, value: V) { | ||||
|     if (this.maxSize || this.maxSize === 0) { | ||||
|       if (this.size >= this.maxSize) return this | ||||
|     } | ||||
| 
 | ||||
|     return super.set(key, value) | ||||
|   } | ||||
| 
 | ||||
|   array() { | ||||
|     return [...this.values()] | ||||
|   } | ||||
| 
 | ||||
|   first(): V { | ||||
|     return this.values().next().value | ||||
|   } | ||||
| 
 | ||||
|   last(): V { | ||||
|     return [...this.values()][this.size - 1] | ||||
|   } | ||||
| 
 | ||||
|   random() { | ||||
|     let arr = [...this.values()] | ||||
|     return arr[Math.floor(Math.random() * arr.length)] | ||||
|   } | ||||
| 
 | ||||
|   find(callback: (value: V, key: K) => boolean) { | ||||
|     for (const key of this.keys()) { | ||||
|       const value = this.get(key)! | ||||
|       if (callback(value, key)) return value | ||||
|     } | ||||
|     // If nothing matched
 | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   filter(callback: (value: V, key: K) => boolean) { | ||||
|     const relevant = new Collection<K, V>() | ||||
|     this.forEach((value, key) => { | ||||
|       if (callback(value, key)) relevant.set(key, value) | ||||
|     }); | ||||
| 
 | ||||
|     return relevant; | ||||
|   } | ||||
| 
 | ||||
|   map<T>(callback: (value: V, key: K) => T) { | ||||
|     const results = [] | ||||
|     for (const key of this.keys()) { | ||||
|       const value = this.get(key)! | ||||
|       results.push(callback(value, key)) | ||||
|     } | ||||
|     return results | ||||
|   } | ||||
| 
 | ||||
|   some(callback: (value: V, key: K) => boolean) { | ||||
|     for (const key of this.keys()) { | ||||
|       const value = this.get(key)! | ||||
|       if (callback(value, key)) return true | ||||
|     } | ||||
| 
 | ||||
|     return false | ||||
|   } | ||||
| 
 | ||||
|   every(callback: (value: V, key: K) => boolean) { | ||||
|     for (const key of this.keys()) { | ||||
|       const value = this.get(key)! | ||||
|       if (!callback(value, key)) return false | ||||
|     } | ||||
| 
 | ||||
|     return true | ||||
|   } | ||||
| 
 | ||||
|   reduce<T>( | ||||
|     callback: (accumulator: T, value: V, key: K) => T, | ||||
|     initialValue?: T, | ||||
|   ): T { | ||||
|     let accumulator: T = initialValue! | ||||
| 
 | ||||
|     for (const key of this.keys()) { | ||||
|       const value = this.get(key)! | ||||
|       accumulator = callback(accumulator, value, key) | ||||
|     } | ||||
| 
 | ||||
|     return accumulator | ||||
|   } | ||||
| } | ||||
							
								
								
									
										3
									
								
								src/utils/delay.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/utils/delay.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | |||
| export const delay = (ms: number) => new Promise((resolve, reject) => { | ||||
|     setTimeout(() => resolve(true), ms); | ||||
| }); | ||||
|  | @ -1,3 +1,3 @@ | |||
| import getChannelByType from './getChannelByType.ts' | ||||
| 
 | ||||
| export default { getChannelByType } | ||||
| export { default as getChannelByType } from './getChannelByType.ts' | ||||
| export type AnyFunction<ReturnType = any> = (...args:any[]) => ReturnType; | ||||
| export { delay } from './delay.ts' | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue