Skip to content
This repository has been archived by the owner on Dec 11, 2024. It is now read-only.

Commit

Permalink
build testing
Browse files Browse the repository at this point in the history
  • Loading branch information
Kathund committed Jul 28, 2024
1 parent adf75ad commit f92b603
Show file tree
Hide file tree
Showing 13 changed files with 339 additions and 20 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,26 @@ jobs:

- name: Check eslint
run: pnpm lint:check

build:
name: build
needs: [pnpm]
runs-on: ubuntu-latest
steps:
- name: Git checkout
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v2

- name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'

- name: Install node dependencies
run: pnpm i

- name: Check eslint
run: pnpm build
8 changes: 5 additions & 3 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
node_modules/
.git
oldreborn/
.github/
.eslintcache
.env
build/
.eslintcache
.git
.gitignore
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"lint:check": "npx eslint --cache src/",
"lint": "npx eslint --cache src/ --fix",
"prettier:check": "npx prettier --cache --check src/",
"prettier": "npx prettier --cache --write src/"
"prettier": "npx prettier --cache --write src/",
"build": "npx tsc"
},
"engines": {
"node": ">=20.16.0",
Expand Down
28 changes: 17 additions & 11 deletions src/Client.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { ClientOptions } from './typings/index';
import Cache from '../Private/defaultCache';
import rateLimit from './Private/rateLimit';
import Cache from './Private/defaultCache';
import Requests from './Private/requests';
import validate from './Private/validate';
import updater from './Private/updater';
import * as API from './API/index';

Check failure on line 7 in src/Client.ts

View workflow job for this annotation

GitHub Actions / build

Cannot find module './API/index' or its corresponding type declarations.
import EventEmitter from 'events';
import Errors from './Errors';
const clients = [];

const clients: any[] = [];

function handleOptions(options: ClientOptions) {
if (!options.cache) options.cache = true;
Expand All @@ -25,9 +27,9 @@ function handleOptions(options: ClientOptions) {

class Client extends EventEmitter {
requests: any;
key: string;
options: ClientOptions;
cache: Cache;
key: string | undefined;
options: ClientOptions | undefined;
cache: Cache | undefined;
constructor(key: string, options: ClientOptions) {
super();
this.requests = new Requests(this, options.cacheHandler);
Expand Down Expand Up @@ -55,10 +57,14 @@ class Client extends EventEmitter {
}

if (this.options.checkForUpdates) {
updater.checkForUpdates().catch(() => {
// eslint-disable-next-line no-console
if (!this.options.silent) console.warn('[hypixel-api-reborn] Error whilst checking for updates!');
});
try {
updater.checkForUpdates();
} catch (error) {
if (this.options && !this.options.silent) {
// eslint-disable-next-line no-console
console.warn('[hypixel-api-reborn] Error whilst checking for updates!');
}
}
}

this.cache = this.requests.cache;
Expand All @@ -78,10 +84,10 @@ class Client extends EventEmitter {
this.emit('outgoingRequest', url, { ...options, headers: { ...options.headers, ...this.options.headers } });

Check failure on line 84 in src/Client.ts

View workflow job for this annotation

GitHub Actions / build

Object is possibly 'undefined'.
const result = await this.requests.request.call(this.requests, url, {
...options,
headers: { ...options.headers, ...this.options.headers }
headers: { ...options.headers, ...this.options?.headers }
});
// eslint-disable-next-line no-underscore-dangle
if (this.options.syncWithHeaders) rateLimit.sync(result._headers);
if (this.options && this.options.syncWithHeaders && result._headers) rateLimit.sync(result._headers);
return result;
}
}
Expand Down
93 changes: 93 additions & 0 deletions src/Private/rateLimit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import Errors from '../Errors';

class RateLimit {
initialized: number;
requests: any;
requestQueue: any;
options: any;
lastResetHappenedAt: any;
resetTimer: any;
cooldownTime: any;
client: any;
static options: any;
static requests: any;
static lastResetHappenedAt: number;
static reset: any;
constructor() {
this.initialized = 0;
}

static async rateLimitManager() {
if (!this.initialized) return;

Check failure on line 21 in src/Private/rateLimit.ts

View workflow job for this annotation

GitHub Actions / build

Property 'initialized' does not exist on type 'typeof RateLimit'.
this.requests++;
this.requestQueue.unshift(Date.now());

Check failure on line 23 in src/Private/rateLimit.ts

View workflow job for this annotation

GitHub Actions / build

Property 'requestQueue' does not exist on type 'typeof RateLimit'.
if ('NONE' === this.options.rateLimit || !this.requestQueue.length) return;

Check failure on line 24 in src/Private/rateLimit.ts

View workflow job for this annotation

GitHub Actions / build

Property 'requestQueue' does not exist on type 'typeof RateLimit'.
if ('AUTO' === this.options.rateLimit && this.requests <= this.options.keyLimit / 2) return;
const cooldown = this.computeCooldownTime();

Check failure on line 26 in src/Private/rateLimit.ts

View workflow job for this annotation

GitHub Actions / build

Property 'computeCooldownTime' does not exist on type 'typeof RateLimit'.
this.requestQueue[0] = Date.now() + cooldown;
return await new Promise((r) => setTimeout(r, cooldown));
}

static sync(data: any): void {
this.options.keyLimit = parseInt(data.get('ratelimit-limit'), 10) || this.options.keyLimit;
this.requests = parseInt(data.get('ratelimit-remaining'), 10) || this.requests;
if (
data.get('ratelimit-reset') &&
Math.round(Date.now() / 1000) - (300 - parseInt(data.get('ratelimit-reset'), 10)) !==
Math.round(this.lastResetHappenedAt / 1000)
) {
clearTimeout(this.resetTimer);
this.resetTimer = setTimeout(this.reset.bind(this), parseInt(data.get('ratelimit-reset'), 10) * 1000);
}
}
static resetTimer(resetTimer: any) {
throw new Error('Method not implemented.');
}

computeCooldownTime() {
const overhead = this.requestQueue[1] <= Date.now() ? 0 : this.requestQueue[1] - Date.now();
const multiplier = Math.floor(this.requests / this.options.keyLimit) + 1;
return (
overhead +
(-overhead - Date.now() + 300000 * multiplier + this.lastResetHappenedAt) /
(this.options.keyLimit * multiplier - this.requests)
);
}

reset() {
this.requests = this.requests - this.options.keyLimit;
if (0 > this.requests) this.requests = 0;
this.lastResetHappenedAt = Date.now();
this.resetTimer = setTimeout(this.reset.bind(this), 300000);
this.requestQueue = this.requestQueue.filter((x: any) => x >= Date.now());
}

rateLimitMonitor() {
this.resetTimer = setTimeout(this.reset.bind(this), 1000 * 300);
}

init(keyInfo: any, options: any, client: any) {
this.options = options;
this.requests = 0;
this.cooldownTime = 300000 / this.options.keyLimit;
this.requestQueue = [];
this.client = client;
return keyInfo
.then((info: any) => {
this.requests = info.requestsInPastMin;
this.lastResetHappenedAt = Date.now() - (300 - info.resetsAfter) * 1000;
this.resetTimer = setTimeout(this.rateLimitMonitor.bind(this), 1000 * info.resetsAfter);
this.initialized = 1;
})
.catch(() => {
client.emit('warn', Errors.RATE_LIMIT_INIT_ERROR);
this.requests = 0;
this.lastResetHappenedAt = Date.now();
this.rateLimitMonitor();
this.initialized = 1;
});
// Still make the requests per min possible
}
}

export default RateLimit;
73 changes: 73 additions & 0 deletions src/Private/requests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
const BASE_URL = 'https://api.hypixel.net/v2';
import Cache from './defaultCache';
import Errors from '../Errors';
import axios from 'axios';

function validateCustomCache(cache: any) {
return Boolean(cache.set && cache.get && cache.delete && cache.keys);
}

class Requests {
cached: any;
client: any;
constructor(client: any, cache: any) {
if (cache && !validateCustomCache(cache)) throw new Error(Errors.INVALID_CACHE_HANDLER);
this.cached = cache || new Cache();
this.client = client;
}
async request(endpoint: any, options: any = {}) {
options.headers = { 'API-Key': this.client.key, ...options.headers };
const res = await axios.get(BASE_URL + endpoint, options);
if (500 <= res.status && 528 > res.status) {
throw new Error(
Errors.ERROR_STATUSTEXT.replace(/{statustext}/, `Server Error : ${res.status} ${res.statusText}`)
);
}
const parsedRes = await res.data.json().catch(() => {
throw new Error(Errors.INVALID_RESPONSE_BODY);
});
if (400 === res.status) {
throw new Error(
Errors.ERROR_CODE_CAUSE.replace(/{code}/, '400 Bad Request').replace(/{cause}/, parsedRes.cause || '')
);
}
if (403 === res.status) throw new Error(Errors.INVALID_API_KEY);
if (422 === res.status) throw new Error(Errors.UNEXPECTED_ERROR);
if (429 === res.status) throw new Error(Errors.RATE_LIMIT_EXCEEDED);
if (200 !== res.status) throw new Error(Errors.ERROR_STATUSTEXT.replace(/{statustext}/, res.statusText));
if (!parsedRes.success && !endpoint.startsWith('/housing')) {
throw new Error(Errors.SOMETHING_WENT_WRONG.replace(/{cause}/, res.statusText));
}
// eslint-disable-next-line no-underscore-dangle
parsedRes._headers = res.headers;
parsedRes.raw = Boolean(options.raw);
if (options.noCaching) return parsedRes;
// split by question mark : first part is /path, remove /
if (this.client.options.cache && this.client.options.cacheFilter(endpoint.split('?')[0].slice(1))) {
if (this.client.options.cacheSize < (await this.cached.size())) {
await this.cached.delete(Array.from(await this.cached.keys())[0]);
}
await this.cached.delete(endpoint);
await this.cached.set(endpoint, parsedRes);
if (0 <= this.client.options.hypixelCacheTime) {
setTimeout(() => this.cached.delete(endpoint), 1000 * this.client.options.hypixelCacheTime);
}
}
return parsedRes;
}

get cache() {
return this.cached;
}

async sweepCache(amount: any) {
if (!amount || amount >= (await this.cached.size())) return await this.cached.clear();
return await Promise.all(
Array.from(await this.cached.keys())
.slice((await this.cached.size()) - amount)
.map((x) => this.cached.delete(x))
);
}
}

export default Requests;
3 changes: 3 additions & 0 deletions src/Private/updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import Errors from '../Errors';
import axios from 'axios';

class Updater implements UpdateHandler {
static checkForUpdates() {
throw new Error('Method not implemented.');
}
async checkForUpdates() {
const request = await axios.get('https://registry.npmjs.org/hypixel-api-reborn');
// eslint-disable-next-line no-console
Expand Down
61 changes: 61 additions & 0 deletions src/Private/validate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { ClientOptions } from '../typings';
import Errors from '../Errors';

/**
* Validation Class, used internally to validate provided arguments
*/
class Validation {
static validateOptions(options: ClientOptions): void {
if ('number' !== typeof options.hypixelCacheTime) throw new Error(Errors.CACHE_TIME_MUST_BE_A_NUMBER);
if ('number' !== typeof options.mojangCacheTime) throw new Error(Errors.CACHE_TIME_MUST_BE_A_NUMBER);
if ('number' !== typeof options.cacheSize) throw new Error(Errors.CACHE_LIMIT_MUST_BE_A_NUMBER);
if ('string' !== typeof options.rateLimit || !['AUTO', 'HARD', 'NONE'].includes(options.rateLimit)) {
throw new Error(Errors.INVALID_RATE_LIMIT_OPTION);
}
if ('number' !== typeof options.keyLimit) throw new Error(Errors.INVALID_KEY_LIMIT_OPTION);
if ('boolean' !== typeof options.syncWithHeaders) throw new Error(Errors.INVALID_HEADER_SYNC_OPTION);
if ('object' !== typeof options.headers) throw new Error(Errors.INVALID_HEADERS);
if ('boolean' !== typeof options.silent) throw new Error(Errors.INVALID_SILENT_OPTION);
if ('boolean' !== typeof options.checkForUpdates) throw new Error(Errors.INVALID_UPDATE_OPTION);
if (!['boolean', 'string'].includes(typeof options.useThirdPartyAPI)) {
throw new Error(Errors.INVALID_THIRD_PARTY_API_OPTION);
}
}

static parseOptions(options: ClientOptions): ClientOptions {
if ('object' !== typeof options || null === options) throw new Error(Errors.OPTIONS_MUST_BE_AN_OBJECT);
return {
cache: options.cache ?? true,
hypixelCacheTime: options.hypixelCacheTime ?? 60,
mojangCacheTime: options.mojangCacheTime ?? 600,
cacheSize: (-1 === options.cacheSize ? Infinity : options.cacheSize) || Infinity,
rateLimit: options.rateLimit ?? 'AUTO',
keyLimit: options.keyLimit ?? 60,
syncWithHeaders: Boolean(options.syncWithHeaders),
headers: options.headers ?? {},
silent: Boolean(options.silent),
checkForUpdates: options.checkForUpdates ?? true,
useThirdPartyAPI: options.useThirdPartyAPI ?? false
};
}

static validateKey(key: string): string {
if (!key) throw new Error(Errors.NO_API_KEY);
if ('string' !== typeof key) throw new Error(Errors.KEY_MUST_BE_A_STRING);
return key;
}

static cacheSuboptions(input: any): boolean {
if ('object' !== typeof input || null === input) return false;
if (!input.noCacheCheck && !input.noCaching && !input.raw) return false;
return true;
}

static validateNodeVersion() {
const versionMatch = process.version.match(/v(\d{2})\.\d{1,}\.\d+/);
const nodeVersion = parseInt(versionMatch ? versionMatch[1] : '', 10);
if (12 > nodeVersion) throw new Error(Errors.NODE_VERSION_ERR);
}
}

export default Validation;
2 changes: 1 addition & 1 deletion src/structures/MiniGames/Arcade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ function parseZombiesKills(data: Record<string, any>): Record<string, number> {
.map((x) => x.match(/^([A-Za-z]+)_zombie_kills_zombies$/))
.filter((x) => x);
// From entries might be broken
return Object.fromEntries(matches.map((x) => [removeSnakeCaseString(x[1]), data[x[0]] || 0]));
return Object.fromEntries(matches.map((x: any) => [removeSnakeCaseString(x[1]), data[x[0]] || 0]));
}
/**
* Zombies - Stats by Map + Difficulty
Expand Down
1 change: 1 addition & 0 deletions src/structures/Player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import VampireZ from './MiniGames/VampireZ';
import Walls from './MiniGames/Walls';
import Warlords from './MiniGames/Warlords';
import WoolWars from './MiniGames/WoolWars';
import PlayerCosmetics from './PlayerCosmetics';

class Player {
nickname: string;
Expand Down
Loading

0 comments on commit f92b603

Please sign in to comment.