mirror of
https://github.com/tiramisulabs/seyfert.git
synced 2025-07-01 20:46:08 +00:00
2.0.5 - first stable version of gateway
This commit is contained in:
parent
9d4b543c8b
commit
a39c06bab9
1
.gitignore
vendored
1
.gitignore
vendored
@ -22,6 +22,7 @@ packages/core/docs.json
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
.vscode
|
||||
|
||||
# testing
|
||||
coverage
|
||||
|
@ -1,6 +1,6 @@
|
||||
# @biscuitland/api-types
|
||||
|
||||
## 2.0.4
|
||||
## 2.0.5
|
||||
|
||||
### Major Changes
|
||||
|
||||
|
@ -25,7 +25,7 @@ import type { DiscordUser } from '@biscuitland/api-types';
|
||||
## Example for [Deno](https://deno.land/)
|
||||
|
||||
```ts
|
||||
import type { DiscordUser } from "https://unpkg.com/@biscuitland/api-types@2.0.4/dist/index.d.ts";
|
||||
import type { DiscordUser } from "https://unpkg.com/@biscuitland/api-types@2.0.5/dist/index.d.ts";
|
||||
```
|
||||
|
||||
We deliver this package through [unpkg](https://unpkg.com/) and it does contain constants and routes too
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@biscuitland/api-types",
|
||||
"version": "2.0.4",
|
||||
"version": "2.0.5",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
|
@ -5,7 +5,7 @@ export const BASE_URL = 'https://discord.com/api';
|
||||
export const API_VERSION = 10;
|
||||
|
||||
/** https://github.com/oasisjs/biscuit/releases */
|
||||
export const BISCUIT_VERSION = '2.0.4';
|
||||
export const BISCUIT_VERSION = '2.0.5';
|
||||
|
||||
/** https://discord.com/developers/docs/reference#user-agent */
|
||||
export const USER_AGENT = `DiscordBot (https://github.com/oasisjs/biscuit, v${BISCUIT_VERSION})`;
|
||||
|
4
packages/cache/CHANGELOG.md
vendored
4
packages/cache/CHANGELOG.md
vendored
@ -1,6 +1,6 @@
|
||||
# @biscuitland/cache
|
||||
|
||||
## 2.0.4
|
||||
## 2.0.5
|
||||
|
||||
### Major Changes
|
||||
|
||||
@ -9,4 +9,4 @@
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @biscuitland/api-types@2.0.4
|
||||
- @biscuitland/api-types@2.0.5
|
||||
|
4
packages/cache/package.json
vendored
4
packages/cache/package.json
vendored
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@biscuitland/cache",
|
||||
"version": "2.0.4",
|
||||
"version": "2.0.5",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
@ -23,7 +23,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@biscuitland/api-types": "^2.0.4",
|
||||
"@biscuitland/api-types": "^2.0.5",
|
||||
"ioredis": "^5.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -1,6 +1,6 @@
|
||||
# @biscuitland/core
|
||||
|
||||
## 2.0.4
|
||||
## 2.0.5
|
||||
|
||||
### Major Changes
|
||||
|
||||
@ -9,6 +9,6 @@
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @biscuitland/api-types@2.0.4
|
||||
- @biscuitland/rest@2.0.4
|
||||
- @biscuitland/ws@2.0.4
|
||||
- @biscuitland/api-types@2.0.5
|
||||
- @biscuitland/rest@2.0.5
|
||||
- @biscuitland/ws@2.0.5
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@biscuitland/core",
|
||||
"version": "2.0.4",
|
||||
"version": "2.0.5",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
@ -23,9 +23,9 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@biscuitland/api-types": "^2.0.4",
|
||||
"@biscuitland/rest": "^2.0.4",
|
||||
"@biscuitland/ws": "^2.0.4"
|
||||
"@biscuitland/api-types": "^2.0.5",
|
||||
"@biscuitland/rest": "^2.0.5",
|
||||
"@biscuitland/ws": "^2.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsup": "^6.1.3"
|
||||
|
@ -10,13 +10,11 @@ import type {
|
||||
Localization,
|
||||
Snowflake,
|
||||
DiscordGetGatewayBot,
|
||||
DiscordGatewayPayload
|
||||
} from '@biscuitland/api-types';
|
||||
|
||||
import { ApplicationCommandTypes, GatewayOpcodes } from '@biscuitland/api-types';
|
||||
import { ApplicationCommandTypes, GatewayOpcodes ,
|
||||
|
||||
// routes
|
||||
|
||||
import {
|
||||
APPLICATION_COMMANDS,
|
||||
GUILD_APPLICATION_COMMANDS,
|
||||
GUILD_APPLICATION_COMMANDS_PERMISSIONS,
|
||||
@ -24,6 +22,8 @@ import {
|
||||
USER
|
||||
} from '@biscuitland/api-types';
|
||||
|
||||
// routes
|
||||
|
||||
import type { PermissionResolvable } from './structures/special/permissions';
|
||||
import type { Activities, StatusTypes } from './structures/presence';
|
||||
|
||||
@ -36,14 +36,13 @@ import { User } from './structures/user';
|
||||
import type { RestAdapter } from '@biscuitland/rest';
|
||||
import { DefaultRestAdapter } from '@biscuitland/rest';
|
||||
|
||||
import type { WsAdapter } from '@biscuitland/ws';
|
||||
import { DefaultWsAdapter } from '@biscuitland/ws';
|
||||
import type { Shard } from '@biscuitland/ws';
|
||||
import { ShardManager } from '@biscuitland/ws';
|
||||
|
||||
import type { EventAdapter } from './adapters/event-adapter';
|
||||
import { DefaultEventAdapter } from './adapters/default-event-adapter';
|
||||
|
||||
import { Util } from './utils/util';
|
||||
import { Shard } from '@biscuitland/ws';
|
||||
|
||||
// PRESENCE
|
||||
|
||||
@ -146,7 +145,7 @@ export interface BiscuitOptions {
|
||||
};
|
||||
|
||||
ws: {
|
||||
adapter?: { new (...args: any[]): WsAdapter };
|
||||
adapter?: { new (...args: any[]): ShardManager };
|
||||
options: any;
|
||||
};
|
||||
}
|
||||
@ -180,9 +179,9 @@ export class Session {
|
||||
options: null,
|
||||
},
|
||||
ws: {
|
||||
adapter: DefaultWsAdapter,
|
||||
adapter: ShardManager,
|
||||
options: null,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
options: BiscuitOptions;
|
||||
@ -190,7 +189,7 @@ export class Session {
|
||||
readonly events: EventAdapter;
|
||||
|
||||
readonly rest: RestAdapter;
|
||||
readonly ws: WsAdapter;
|
||||
readonly ws: ShardManager;
|
||||
|
||||
private adapters = new Map<string, any>();
|
||||
|
||||
@ -210,20 +209,17 @@ export class Session {
|
||||
|
||||
// makeWs
|
||||
|
||||
const defHandler: DiscordRawEventHandler = (shard, event) => {
|
||||
let data = event as any;
|
||||
// let data = JSON.parse(message) as DiscordGatewayPayload;
|
||||
const defHandler = (shard: Shard, payload: DiscordGatewayPayload) => {
|
||||
Actions.raw(this, shard.options.id, payload);
|
||||
|
||||
Actions.raw(this, shard.id, data);
|
||||
|
||||
if (!data.t || !data.d) {
|
||||
if (!payload.t || !payload.d) {
|
||||
return;
|
||||
}
|
||||
|
||||
Actions[data.t as keyof typeof Actions]?.(
|
||||
Actions[payload.t as keyof typeof Actions]?.(
|
||||
this,
|
||||
shard.id,
|
||||
data.d as any
|
||||
shard.options.id,
|
||||
payload.d as any
|
||||
);
|
||||
};
|
||||
|
||||
@ -231,21 +227,28 @@ export class Session {
|
||||
this.options.ws.options = {
|
||||
handleDiscordPayload: defHandler,
|
||||
|
||||
gatewayConfig: {
|
||||
token: this.options.token,
|
||||
intents: this.options.intents,
|
||||
},
|
||||
gateway: {
|
||||
url: '',
|
||||
shards: '',
|
||||
|
||||
intents: this.options.intents,
|
||||
token: this.options.token,
|
||||
session_start_limit: {
|
||||
total: 1000,
|
||||
remaining: 1000,
|
||||
reset_after: 3600000,
|
||||
|
||||
max_concurrency: 1
|
||||
}
|
||||
},
|
||||
config: {
|
||||
token: this.options.token,
|
||||
intents: this.options.intents,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// makeEvents
|
||||
|
||||
this.events = this.options.events?.adapter
|
||||
? new this.options.events.adapter()
|
||||
: new DefaultEventAdapter();
|
||||
this.events = this.options.events?.adapter ? new this.options.events.adapter() : new DefaultEventAdapter();
|
||||
|
||||
this.ws = this.getWs();
|
||||
this.token = options.token;
|
||||
@ -282,7 +285,7 @@ export class Session {
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
private getWs(): WsAdapter {
|
||||
private getWs(): ShardManager {
|
||||
return this.getAdapter(
|
||||
this.options.ws.adapter!,
|
||||
this.options.ws.options
|
||||
@ -294,23 +297,10 @@ export class Session {
|
||||
*/
|
||||
|
||||
async start(): Promise<void> {
|
||||
const nonParsed = await this.rest.get<DiscordGetGatewayBot>('/gateway/bot');
|
||||
const gateway = await this.rest.get<DiscordGetGatewayBot>('/gateway/bot');
|
||||
this.ws.options.gateway = gateway;
|
||||
|
||||
this.ws.options.gatewayBot = {
|
||||
url: nonParsed.url,
|
||||
shards: nonParsed.shards,
|
||||
sessionStartLimit: {
|
||||
total: nonParsed.session_start_limit.total,
|
||||
remaining: nonParsed.session_start_limit.remaining,
|
||||
resetAfter: nonParsed.session_start_limit.reset_after,
|
||||
maxConcurrency: nonParsed.session_start_limit.max_concurrency,
|
||||
},
|
||||
};
|
||||
|
||||
this.ws.options.lastShardId = this.ws.options.gatewayBot.shards - 1;
|
||||
this.ws.agent.options.totalShards = this.ws.options.gatewayBot.shards;
|
||||
|
||||
this.ws.shards();
|
||||
this.ws.spawns();
|
||||
}
|
||||
|
||||
// USEFUL METHODS
|
||||
@ -338,7 +328,7 @@ export class Session {
|
||||
* }
|
||||
*/
|
||||
editStatus(shardId: number, status: StatusUpdate, prio = true): void {
|
||||
const shard = this.ws.agent.shards.get(shardId);
|
||||
const shard = this.ws.shards.get(shardId);
|
||||
|
||||
if (!shard) {
|
||||
throw new Error(`Unknown shard ${shardId}`);
|
||||
@ -350,7 +340,7 @@ export class Session {
|
||||
status: status.status,
|
||||
since: null,
|
||||
afk: false,
|
||||
activities: status.activities.map((activity) => {
|
||||
activities: status.activities.map(activity => {
|
||||
return {
|
||||
name: activity.name,
|
||||
type: activity.type,
|
||||
@ -399,7 +389,7 @@ export class Session {
|
||||
permissions: options,
|
||||
},
|
||||
{
|
||||
headers: { 'Authorization': `Bearer ${bearerToken}` },
|
||||
headers: { Authorization: `Bearer ${bearerToken}` },
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -463,7 +453,7 @@ export class Session {
|
||||
guildId
|
||||
? GUILD_APPLICATION_COMMANDS(this.applicationId, guildId)
|
||||
: APPLICATION_COMMANDS(this.applicationId),
|
||||
options.map((o) =>
|
||||
options.map(o =>
|
||||
this.isContextApplicationCommand(o)
|
||||
? {
|
||||
name: o.name,
|
||||
|
@ -1,9 +1,9 @@
|
||||
# @biscuitland/helpers
|
||||
|
||||
## 2.0.4
|
||||
## 2.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @biscuitland/api-types@2.0.4
|
||||
- @biscuitland/core@2.0.4
|
||||
- @biscuitland/api-types@2.0.5
|
||||
- @biscuitland/core@2.0.5
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@biscuitland/helpers",
|
||||
"version": "2.0.4",
|
||||
"version": "2.0.5",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
@ -23,8 +23,8 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@biscuitland/api-types": "^2.0.4",
|
||||
"@biscuitland/core": "^2.0.4"
|
||||
"@biscuitland/api-types": "^2.0.5",
|
||||
"@biscuitland/core": "^2.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsup": "^6.1.3"
|
||||
|
@ -1,6 +1,6 @@
|
||||
# @biscuitland/rest
|
||||
|
||||
## 2.0.4
|
||||
## 2.0.5
|
||||
|
||||
### Major Changes
|
||||
|
||||
@ -9,4 +9,4 @@
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @biscuitland/api-types@2.0.4
|
||||
- @biscuitland/api-types@2.0.5
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@biscuitland/rest",
|
||||
"version": "2.0.4",
|
||||
"version": "2.0.5",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
@ -23,7 +23,7 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@biscuitland/api-types": "^2.0.4"
|
||||
"@biscuitland/api-types": "^2.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsup": "^6.1.3"
|
||||
|
@ -1,6 +1,6 @@
|
||||
# @biscuitland/ws
|
||||
|
||||
## 2.0.4
|
||||
## 2.0.5
|
||||
|
||||
### Major Changes
|
||||
|
||||
@ -9,4 +9,4 @@
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @biscuitland/api-types@2.0.4
|
||||
- @biscuitland/api-types@2.0.5
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@biscuitland/ws",
|
||||
"version": "2.0.4",
|
||||
"version": "2.0.5",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.ts",
|
||||
@ -23,7 +23,8 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@biscuitland/api-types": "^2.0.4",
|
||||
"@biscuitland/api-types": "^2.0.5",
|
||||
"@sapphire/async-queue": "^1.5.0",
|
||||
"ws": "^8.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -1,234 +0,0 @@
|
||||
import { createLeakyBucket } from '../utils/bucket-util';
|
||||
|
||||
import type { LeakyBucket } from '../utils/bucket-util';
|
||||
|
||||
import type { GatewayBot, PickPartial } from '@biscuitland/api-types';
|
||||
import type { WsAdapter } from './ws-adapter';
|
||||
|
||||
import type {
|
||||
Shard,
|
||||
ShardGatewayConfig,
|
||||
ShardOptions,
|
||||
} from '../services/shard';
|
||||
|
||||
import { Agent } from '../services/agent';
|
||||
|
||||
export class DefaultWsAdapter implements WsAdapter {
|
||||
static readonly DEFAULTS = {
|
||||
spawnShardDelay: 5300,
|
||||
|
||||
shardsPerWorker: 25,
|
||||
totalWorkers: 4,
|
||||
|
||||
gatewayBot: {
|
||||
url: 'wss://gateway.discord.gg',
|
||||
shards: 1,
|
||||
|
||||
sessionStartLimit: {
|
||||
total: 1000,
|
||||
remaining: 1000,
|
||||
resetAfter: 0,
|
||||
maxConcurrency: 1,
|
||||
},
|
||||
},
|
||||
|
||||
firstShardId: 0,
|
||||
|
||||
lastShardId: 1,
|
||||
};
|
||||
|
||||
buckets = new Map<
|
||||
number,
|
||||
{
|
||||
workers: { id: number; queue: number[] }[];
|
||||
leak: LeakyBucket;
|
||||
}
|
||||
>();
|
||||
|
||||
options: Options;
|
||||
|
||||
agent: Agent;
|
||||
|
||||
constructor(options: DefaultWsOptions) {
|
||||
this.options = Object.assign(Object.create(DefaultWsAdapter.DEFAULTS), options);
|
||||
|
||||
this.options.firstShardId = this.options.firstShardId ?? 0;
|
||||
this.options.lastShardId = this.options.lastShardId ?? this.options.totalShards - 1 ?? 1;
|
||||
|
||||
this.agent = new Agent({
|
||||
totalShards: this.options.totalShards ?? this.options.gatewayBot.shards ?? 1,
|
||||
gatewayConfig: this.options.gatewayConfig,
|
||||
createShardOptions: this.options.createShardOptions,
|
||||
|
||||
handleMessage: (shard: Shard, message: MessageEvent<any>) => {
|
||||
return this.options.handleDiscordPayload(shard, message);
|
||||
},
|
||||
|
||||
handleIdentify: (id: number) => {
|
||||
return this.buckets.get(id % this.options.gatewayBot.sessionStartLimit.maxConcurrency)!.leak.acquire(1);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
prepareBuckets() {
|
||||
for (
|
||||
let i = 0;
|
||||
i < this.options.gatewayBot.sessionStartLimit.maxConcurrency;
|
||||
++i
|
||||
) {
|
||||
this.buckets.set(i, {
|
||||
workers: [],
|
||||
leak: createLeakyBucket({
|
||||
max: 1,
|
||||
refillAmount: 1,
|
||||
refillInterval: this.options.spawnShardDelay,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
for (
|
||||
let shardId = this.options.firstShardId;
|
||||
shardId <= this.options.lastShardId;
|
||||
++shardId
|
||||
) {
|
||||
if (shardId >= this.agent.options.totalShards) {
|
||||
throw new Error(
|
||||
`Shard (id: ${shardId}) is bigger or equal to the used amount of used shards which is ${this.agent.options.totalShards}`
|
||||
);
|
||||
}
|
||||
|
||||
const bucketId = shardId % this.options.gatewayBot.sessionStartLimit.maxConcurrency;
|
||||
const bucket = this.buckets.get(bucketId);
|
||||
|
||||
if (!bucket) {
|
||||
throw new Error(
|
||||
`Shard (id: ${shardId}) got assigned to an illegal bucket id: ${bucketId}, expected a bucket id between 0 and ${
|
||||
this.options.gatewayBot.sessionStartLimit
|
||||
.maxConcurrency - 1
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
const workerId = this.workerId(shardId);
|
||||
const worker = bucket.workers.find(w => w.id === workerId);
|
||||
|
||||
if (worker) {
|
||||
worker.queue.push(shardId);
|
||||
} else {
|
||||
bucket.workers.push({ id: workerId, queue: [shardId] });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
prepareShards() {
|
||||
this.buckets.forEach((bucket, bucketId) => {
|
||||
for (const worker of bucket.workers) {
|
||||
for (const shardId of worker.queue) {
|
||||
this.workerToIdentify(worker.id, shardId, bucketId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
calculateTotalShards(): number {
|
||||
if (this.agent.options.totalShards < 100) {
|
||||
return this.agent.options.totalShards;
|
||||
}
|
||||
|
||||
return (
|
||||
Math.ceil(
|
||||
this.agent.options.totalShards /
|
||||
(this.options.gatewayBot.sessionStartLimit
|
||||
.maxConcurrency === 1
|
||||
? 16
|
||||
: this.options.gatewayBot.sessionStartLimit
|
||||
.maxConcurrency)
|
||||
) * this.options.gatewayBot.sessionStartLimit.maxConcurrency
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
workerToIdentify(_workerId: number, shardId: number, _bucketId: number) {
|
||||
return this.agent.identify(shardId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
workerId(shardId: number) {
|
||||
let workerId = Math.floor(shardId / this.options.shardsPerWorker);
|
||||
|
||||
if (workerId >= this.options.totalWorkers) {
|
||||
workerId = this.options.totalWorkers - 1;
|
||||
}
|
||||
|
||||
return workerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
shards() {
|
||||
this.prepareBuckets();
|
||||
|
||||
this.prepareShards();
|
||||
}
|
||||
}
|
||||
|
||||
export type DefaultWsOptions = Pick<
|
||||
Options,
|
||||
Exclude<keyof Options, keyof typeof DefaultWsAdapter.DEFAULTS>
|
||||
> &
|
||||
Partial<Options>;
|
||||
|
||||
interface Options {
|
||||
/** Delay in milliseconds to wait before spawning next shard. */
|
||||
spawnShardDelay: number;
|
||||
|
||||
/** The amount of shards to load per worker. */
|
||||
shardsPerWorker: number;
|
||||
|
||||
/** The total amount of workers to use for your bot. */
|
||||
totalWorkers: number;
|
||||
|
||||
/** Total amount of shards your bot uses. Useful for zero-downtime updates or resharding. */
|
||||
totalShards: number;
|
||||
|
||||
/** Id of the first Shard which should get controlled by this manager. */
|
||||
firstShardId: number;
|
||||
|
||||
/** Id of the last Shard which should get controlled by this manager. */
|
||||
lastShardId: number;
|
||||
|
||||
createShardOptions?: Omit<
|
||||
ShardOptions,
|
||||
'id' | 'totalShards' | 'requestIdentify' | 'gatewayConfig'
|
||||
>;
|
||||
|
||||
/** Important data which is used by the manager to connect shards to the gateway. */
|
||||
gatewayBot: GatewayBot;
|
||||
|
||||
// REMOVE THIS
|
||||
|
||||
gatewayConfig: PickPartial<ShardGatewayConfig, 'token'>;
|
||||
|
||||
/** Sends the discord payload to another guild. */
|
||||
handleDiscordPayload: (shard: Shard, data: MessageEvent<any>) => any;
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
import type { Agent } from '../services/agent';
|
||||
import type { GatewayBot } from '@biscuitland/api-types';
|
||||
|
||||
export interface WsAdapter {
|
||||
options: Partial<Options | any>;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
agent: Agent;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
shards(): void;
|
||||
}
|
||||
|
||||
interface Options {
|
||||
/** Id of the first Shard which should get controlled by this manager. */
|
||||
firstShardId: number;
|
||||
|
||||
/** Id of the last Shard which should get controlled by this manager. */
|
||||
lastShardId: number;
|
||||
|
||||
/** Important data which is used by the manager to connect shards to the gateway. */
|
||||
gatewayBot: GatewayBot;
|
||||
}
|
@ -1,9 +1,2 @@
|
||||
export { WsAdapter } from './adapters/ws-adapter';
|
||||
|
||||
export {
|
||||
DefaultWsAdapter,
|
||||
DefaultWsOptions,
|
||||
} from './adapters/default-ws-adapter';
|
||||
|
||||
export { AgentOptions, Agent } from './services/agent';
|
||||
export { ShardManager } from './services/shard-manager';
|
||||
export { Shard } from './services/shard';
|
||||
|
@ -1,113 +0,0 @@
|
||||
import type { ShardGatewayConfig, ShardOptions } from './shard';
|
||||
import type { PickPartial } from '@biscuitland/api-types';
|
||||
|
||||
import { Shard } from './shard';
|
||||
|
||||
export class Agent {
|
||||
static readonly DEFAULTS = {
|
||||
shardIds: [],
|
||||
|
||||
totalShards: 1,
|
||||
};
|
||||
|
||||
options: Options;
|
||||
shards: Map<number, Shard>;
|
||||
|
||||
constructor(options: AgentOptions) {
|
||||
this.options = Object.assign(Agent.DEFAULTS, options);
|
||||
|
||||
const { handleIdentify } = this.options;
|
||||
|
||||
this.shards = new Map(
|
||||
this.options.shardIds.map(id => {
|
||||
const shard = new Shard({
|
||||
id,
|
||||
totalShards: this.options.totalShards,
|
||||
gatewayConfig: this.options.gatewayConfig,
|
||||
|
||||
handleMessage: (shard, message) => {
|
||||
return this.options.handleMessage(shard, message);
|
||||
},
|
||||
|
||||
async handleIdentify() {
|
||||
return await handleIdentify(id);
|
||||
},
|
||||
|
||||
...this.options.createShardOptions,
|
||||
});
|
||||
|
||||
return [id, shard] as const;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
async identify(id: number) {
|
||||
// @ts-ignore
|
||||
let shard = this.shards.get(id);
|
||||
|
||||
if (!shard) {
|
||||
const { handleIdentify } = this.options;
|
||||
|
||||
shard = new Shard({
|
||||
id,
|
||||
totalShards: this.options.totalShards,
|
||||
gatewayConfig: this.options.gatewayConfig,
|
||||
|
||||
handleMessage: (shard, message) => {
|
||||
return this.options.handleMessage(shard, message);
|
||||
},
|
||||
|
||||
async handleIdentify() {
|
||||
return await handleIdentify(id);
|
||||
},
|
||||
|
||||
...this.options.createShardOptions,
|
||||
});
|
||||
|
||||
this.shards.set(id, shard);
|
||||
}
|
||||
|
||||
return await shard.identify();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
|
||||
async scale() {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
export type AgentOptions = Pick<
|
||||
Options,
|
||||
Exclude<keyof Options, keyof typeof Agent.DEFAULTS>
|
||||
> &
|
||||
Partial<Options>;
|
||||
|
||||
interface Options {
|
||||
/** Ids of the Shards which should be managed. */
|
||||
shardIds: number[];
|
||||
|
||||
/** Total amount of Shard used by the bot. */
|
||||
totalShards: number;
|
||||
|
||||
/** Options which are used to create a new Shard. */
|
||||
createShardOptions?: Omit<
|
||||
ShardOptions,
|
||||
'id' | 'totalShards' | 'requestIdentify' | 'gatewayConfig'
|
||||
>;
|
||||
|
||||
/** Gateway configuration which is used when creating a Shard. */
|
||||
gatewayConfig: PickPartial<ShardGatewayConfig, 'token'>;
|
||||
|
||||
/** Sends the discord payload to another guild. */
|
||||
handleMessage: (shard: Shard, data: MessageEvent<any>) => any;
|
||||
|
||||
/** This function communicates with the parent manager. */
|
||||
handleIdentify(shardId: number): Promise<void>;
|
||||
}
|
121
packages/ws/src/services/shard-manager.ts
Normal file
121
packages/ws/src/services/shard-manager.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import type { ShardManagerOptions, SMO } from '../types';
|
||||
import type { LeakyBucket } from '../utils/bucket';
|
||||
|
||||
import { Shard } from './shard';
|
||||
|
||||
import { createLeakyBucket } from '../utils/bucket';
|
||||
import { Options } from '../utils/options';
|
||||
|
||||
export class ShardManager {
|
||||
static readonly DEFAULTS = {
|
||||
workers: {
|
||||
shards: 25,
|
||||
amount: 5,
|
||||
delay: 5000
|
||||
},
|
||||
|
||||
shards: {
|
||||
timeout: 15000,
|
||||
delay: 5000
|
||||
}
|
||||
};
|
||||
|
||||
readonly options: SMO;
|
||||
|
||||
readonly buckets = new Map<
|
||||
number,
|
||||
{
|
||||
workers: { id: number; queue: number[] }[];
|
||||
leak: LeakyBucket;
|
||||
}
|
||||
>();
|
||||
|
||||
readonly shards = new Map<number, Shard>();
|
||||
|
||||
constructor(options: ShardManagerOptions) {
|
||||
this.options = Options({}, ShardManager.DEFAULTS, options);
|
||||
}
|
||||
|
||||
/** Invokes internal processing and respawns shards */
|
||||
async respawns(): Promise<void> {
|
||||
//
|
||||
}
|
||||
|
||||
/** Invoke internal processing and spawns shards */
|
||||
async spawns(): Promise<void> {
|
||||
const { gateway, workers } = this.options;
|
||||
|
||||
/** Creates the necessary buckets according to concurrency */
|
||||
for (let i = 0; i < gateway.session_start_limit.max_concurrency; i++) {
|
||||
this.buckets.set(i, {
|
||||
workers: [],
|
||||
leak: createLeakyBucket({
|
||||
max: 1,
|
||||
refillAmount: 1,
|
||||
refillInterval: workers.delay,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/** Create the start sequence of the shards inside the buckets. */
|
||||
for (let i = 0; i < gateway.shards; i++) {
|
||||
const bucketID = i % gateway.session_start_limit.max_concurrency;
|
||||
const bucket = this.buckets.get(bucketID);
|
||||
|
||||
if (bucket) {
|
||||
const workerID = Math.floor(i / workers.shards);
|
||||
const worker = bucket.workers.find(w => w.id === workerID);
|
||||
|
||||
if (worker) {
|
||||
worker.queue.push(i);
|
||||
} else {
|
||||
bucket.workers.push({ id: workerID, queue: [i] });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Route all shards to workers */
|
||||
this.buckets.forEach(async bucket => {
|
||||
for (const worker of bucket.workers) {
|
||||
|
||||
for (const id of worker.queue) {
|
||||
await this.connect(id);
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Invokes the bucket to prepare the connection to the shard */
|
||||
private async connect(id: number): Promise<Shard> {
|
||||
const { gateway } = this.options;
|
||||
|
||||
let shard = this.shards.get(id);
|
||||
|
||||
if (!shard) {
|
||||
shard = new Shard({
|
||||
id,
|
||||
|
||||
gateway: this.options.gateway,
|
||||
|
||||
shards: this.options.shards,
|
||||
|
||||
config: this.options.config,
|
||||
|
||||
handlePayloads: async (shard, payload) => {
|
||||
await this.options.handleDiscordPayload(shard, payload); // remove await?
|
||||
},
|
||||
|
||||
handleIdentify: async (id: number) => {
|
||||
await this.buckets.get(id % gateway.session_start_limit.max_concurrency)!.leak.acquire(1); // remove await?
|
||||
}
|
||||
});
|
||||
|
||||
this.shards.set(id, shard);
|
||||
}
|
||||
|
||||
await shard.connect();
|
||||
|
||||
return shard;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
91
packages/ws/src/types.ts
Normal file
91
packages/ws/src/types.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import type { DiscordGatewayPayload, DiscordGetGatewayBot, GatewayIntents } from '@biscuitland/api-types';
|
||||
|
||||
import type { ShardManager } from './services/shard-manager';
|
||||
import type { Shard } from './services/shard';
|
||||
|
||||
/** ShardManager */
|
||||
|
||||
export type ShardManagerOptions = Pick<SMO, Exclude<keyof SMO, keyof typeof ShardManager.DEFAULTS>> & Partial<SMO>;
|
||||
|
||||
export interface SMO {
|
||||
/** Function for interpretation of messages from discord */
|
||||
handleDiscordPayload: (shard: Shard, payload: DiscordGatewayPayload) => unknown;
|
||||
|
||||
/** Based on the information in Get Gateway */
|
||||
gateway: DiscordGetGatewayBot;
|
||||
|
||||
/** Workers options */
|
||||
workers: ShardManagerWorkersOptions;
|
||||
|
||||
/** Authentication */
|
||||
config: {
|
||||
intents?: GatewayIntents;
|
||||
token: string;
|
||||
};
|
||||
|
||||
/** Options shards */
|
||||
shards: ShardManagerShardsOptions;
|
||||
}
|
||||
|
||||
export interface ShardManagerWorkersOptions {
|
||||
/**
|
||||
* Number of shards per worker
|
||||
* @default 25
|
||||
*/
|
||||
shards: number;
|
||||
|
||||
/**
|
||||
* Number of workers
|
||||
* @default 5
|
||||
*/
|
||||
amount: number;
|
||||
|
||||
/**
|
||||
* Waiting time between workers
|
||||
* @default 5000
|
||||
*/
|
||||
delay: number;
|
||||
}
|
||||
|
||||
export interface ShardManagerShardsOptions {
|
||||
/**
|
||||
* Waiting time to receive the ready event.
|
||||
* @default 15000
|
||||
*/
|
||||
timeout: number;
|
||||
|
||||
/**
|
||||
* Waiting time between shards
|
||||
* @default 5000
|
||||
*/
|
||||
delay: number;
|
||||
}
|
||||
|
||||
/** Shard */
|
||||
|
||||
export type ShardOptions = Pick<SO, Exclude<keyof SO, keyof typeof Shard.DEFAULTS>> & Partial<SO>;
|
||||
|
||||
export interface SO {
|
||||
/** Shard Id */
|
||||
id: number;
|
||||
|
||||
/** Based on the information in Get Gateway */
|
||||
gateway: DiscordGetGatewayBot;
|
||||
|
||||
/** Options shards */
|
||||
shards: ShardManagerShardsOptions;
|
||||
|
||||
/** Authentication */
|
||||
config: {
|
||||
intents?: GatewayIntents;
|
||||
token: string;
|
||||
};
|
||||
|
||||
/** Function for interpretation of messages from discord */
|
||||
handlePayloads: (shard: Shard, data: DiscordGatewayPayload) => Promise<void>;
|
||||
|
||||
/** Notify the manager if the shard is ready. */
|
||||
handleIdentify: (id: number) => Promise<void>;
|
||||
}
|
||||
|
||||
export type ShardStatus = 'Disconnected' | 'Handshaking' | 'Connecting' | 'Heartbeating' | 'Identifying' | 'Resuming' | 'Ready';
|
@ -1,4 +1,4 @@
|
||||
/** unnecessary */
|
||||
/** Create from scratch */
|
||||
|
||||
import type { PickPartial } from '@biscuitland/api-types';
|
||||
|
42
packages/ws/src/utils/options.ts
Normal file
42
packages/ws/src/utils/options.ts
Normal file
@ -0,0 +1,42 @@
|
||||
const isPlainObject = (value: any) => {
|
||||
return (
|
||||
value !== null
|
||||
&& typeof value === 'object'
|
||||
&& typeof value.constructor === 'function'
|
||||
// eslint-disable-next-line no-prototype-builtins
|
||||
&& (value.constructor.prototype.hasOwnProperty('isPrototypeOf') || Object.getPrototypeOf(value.constructor.prototype) === null)
|
||||
)
|
||||
|| (value && Object.getPrototypeOf(value) === null);
|
||||
};
|
||||
|
||||
const isObject = (o: any) => {
|
||||
return !!o && typeof o === 'object' && !Array.isArray(o);
|
||||
};
|
||||
|
||||
export const Options = (defaults: any, ...options: any[]): any => {
|
||||
if (!options.length) {
|
||||
return defaults;
|
||||
}
|
||||
|
||||
const source = options.shift();
|
||||
|
||||
if (isObject(defaults) && isPlainObject(source)) {
|
||||
Object.entries(source).forEach(([key, value]) => {
|
||||
if (typeof value === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPlainObject(value)) {
|
||||
if (!(key in defaults)) {
|
||||
Object.assign(defaults, { [key]: {} });
|
||||
}
|
||||
|
||||
Options(defaults[key], value);
|
||||
} else {
|
||||
Object.assign(defaults, { [key]: value });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Options(defaults, ...options);
|
||||
};
|
@ -1,18 +0,0 @@
|
||||
/** unnecessary */
|
||||
|
||||
import type { Shard } from '../services/shard';
|
||||
|
||||
export async function checkOffline(
|
||||
shard: Shard,
|
||||
highPriority: boolean
|
||||
): Promise<void> {
|
||||
if (!shard.isOpen()) {
|
||||
await new Promise(resolve => {
|
||||
if (highPriority) {
|
||||
shard.offlineSendQueue.unshift(resolve);
|
||||
} else {
|
||||
shard.offlineSendQueue.push(resolve);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user