generated from tiramisulabs/monorepo
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
423 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
A custom implementation of [limiter](https://github.com/jhurliman/node-rate-limiter) for cooldown in seyfert |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
{ | ||
"name": "@slipher/cooldown", | ||
"version": "0.0.1", | ||
"private": false, | ||
"license": "MIT", | ||
|
||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"files": [ | ||
"lib/**" | ||
], | ||
"main": "./lib/index.js", | ||
"module": "./lib/index.js", | ||
"types": "./lib/index.d.ts", | ||
"scripts": { | ||
"dev": "tsc --watch", | ||
"build": "tsc", | ||
"lint": "biome lint --write ./src", | ||
"format": "biome format --write ./src" | ||
}, | ||
"devDependencies": { | ||
"@types/node": "^22.4.0", | ||
"typescript": "^5.5.4" | ||
}, | ||
"peerDependencies": { | ||
"seyfert": "^2.0.0" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
// https://github.com/jhurliman/node-rate-limiter/blob/main/src/clock.ts | ||
|
||
// generate timestamp or delta | ||
// see http://nodejs.org/api/process.html#process_process_hrtime | ||
function hrtime(previousTimestamp?: [number, number]): [number, number] { | ||
const clocktime = performance.now() * 1e-3; | ||
let seconds = Math.floor(clocktime); | ||
let nanoseconds = Math.floor((clocktime % 1) * 1e9); | ||
if (previousTimestamp !== undefined) { | ||
seconds -= previousTimestamp[0]; | ||
nanoseconds -= previousTimestamp[1]; | ||
if (nanoseconds < 0) { | ||
seconds--; | ||
nanoseconds += 1e9; | ||
} | ||
} | ||
return [seconds, nanoseconds]; | ||
} | ||
|
||
// The current timestamp in whole milliseconds | ||
export function getMilliseconds(): number { | ||
const [seconds, nanoseconds] = hrtime(); | ||
return seconds * 1e3 + Math.floor(nanoseconds / 1e6); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import type { CooldownProps } from './manager'; | ||
|
||
export * from './manager'; | ||
export * from './resource'; | ||
|
||
export function Cooldown(props: CooldownProps) { | ||
return <T extends { new (...args: any[]): {} }>(target: T) => | ||
class extends target { | ||
cooldown = props; | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
import type { UsingClient } from 'seyfert'; | ||
import { type CooldownData, type CooldownDataInsert, Cooldowns, type CooldownType } from './resource'; | ||
import { getMilliseconds } from './clock'; | ||
|
||
export class CooldownManager { | ||
resource: Cooldowns; | ||
constructor(readonly client: UsingClient) { | ||
this.resource = new Cooldowns(client.cache, client); | ||
} | ||
|
||
/** | ||
* Get the cooldown data for a command | ||
* @param name - The name of the command | ||
* @returns The cooldown data for the command | ||
*/ | ||
getData(name: string): CooldownProps | undefined { | ||
return this.client.commands?.values.find(x => x.name === name)?.cooldown; | ||
} | ||
|
||
/** | ||
* Check if a user has a cooldown | ||
* @param name - The name of the command | ||
* @param target - The target of the cooldown | ||
* @returns Whether the user has a cooldown | ||
*/ | ||
has(name: string, target: string) { | ||
const data = this.getData(name); | ||
if (!data) return false; | ||
|
||
const cooldown = this.resource.get(`${name}:${data.type}:${target}`); | ||
if (!cooldown) { | ||
this.set(name, target, { type: data.type, interval: data.interval, remaining: data.refill - data.tokens }); | ||
return false; | ||
} | ||
|
||
const remaining = cooldown.remaining - data.tokens; | ||
|
||
if (remaining <= 0) return true; | ||
return false; | ||
} | ||
|
||
/** | ||
* Use a cooldown | ||
* @param name - The name of the command | ||
* @param target - The target of the cooldown | ||
* @param tokens - The number of tokens to use | ||
* @returns The remaining cooldown | ||
*/ | ||
use(name: string, target: string, tokens?: number) { | ||
const data = this.getData(name); | ||
if (!data) return; | ||
|
||
const cooldown = this.resource.get(`${name}:${data.type}:${target}`); | ||
if (!cooldown) { | ||
this.set(name, target, { | ||
type: data.type, | ||
interval: data.interval, | ||
remaining: data.refill - (tokens ?? data.tokens), | ||
}); | ||
return true; | ||
} | ||
|
||
const drip = this.drip(name, target, data, cooldown); | ||
|
||
if (drip.remaining <= 0) return false; | ||
|
||
return true; | ||
} | ||
|
||
/** | ||
* Refill the cooldown | ||
* @param name - The name of the command | ||
* @param target - The target of the cooldown | ||
* @returns Whether the cooldown was refilled | ||
*/ | ||
refill(name: string, target: string, tokens?: number) { | ||
const data = this.getData(name); | ||
if (!data) return false; | ||
|
||
const refill = tokens ?? data.refill; | ||
|
||
const cooldown = this.resource.get(`${name}:${data.type}:${target}`); | ||
if (!cooldown) { | ||
this.set(name, target, { | ||
type: data.type, | ||
interval: data.interval, | ||
remaining: refill, | ||
}); | ||
return true; | ||
} | ||
|
||
this.set(name, target, { type: data.type, interval: data.interval, remaining: refill }); | ||
return true; | ||
} | ||
|
||
/** | ||
* Set the cooldown data for a command | ||
* @param name - The name of the command | ||
* @param target - The target of the cooldown | ||
* @param data - The cooldown data to set | ||
*/ | ||
set(name: string, target: string, data: CooldownDataInsert & { type: `${CooldownType}` }) { | ||
this.resource.set(`${name}:${data.type}:${target}`, data); | ||
} | ||
|
||
/** | ||
* Drip the cooldown | ||
* @param name - The name of the command | ||
* @param target - The target of the cooldown | ||
* @param props - The cooldown properties | ||
* @param data - The cooldown data | ||
* @returns The remaining cooldown | ||
*/ | ||
drip(name: string, target: string, props: CooldownProps, data: CooldownData) { | ||
const now = getMilliseconds(); | ||
const deltaMS = Math.max(now - data.lastDrip, 0); | ||
data.lastDrip = now; | ||
|
||
const dripAmount = deltaMS * (props.refill / props.interval); | ||
data.remaining = Math.min(data.remaining + dripAmount, props.refill); | ||
const result = { type: props.type, interval: props.interval, remaining: data.remaining }; | ||
this.set(name, target, result); | ||
return result; | ||
} | ||
} | ||
|
||
export interface CooldownProps { | ||
/** target type */ | ||
type: `${CooldownType}`; | ||
/** interval in ms */ | ||
interval: number; | ||
/** refill amount */ | ||
refill: number; | ||
/** tokens to use */ | ||
tokens: number; | ||
/** byPass users */ | ||
byPass?: string[]; | ||
} | ||
|
||
declare module 'seyfert' { | ||
interface Command { | ||
cooldown?: CooldownProps; | ||
} | ||
interface SubCommand { | ||
cooldown?: CooldownProps; | ||
} | ||
interface ContextMenuCommand { | ||
cooldown?: CooldownProps; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { BaseResource } from "seyfert/lib/cache/resources/default/base"; | ||
import { getMilliseconds } from "./clock"; | ||
|
||
export interface CooldownData { | ||
remaining: number; | ||
interval: number; | ||
lastDrip: number; | ||
} | ||
|
||
export type CooldownDataInsert = Omit<CooldownData, 'lastDrip'>; | ||
|
||
export enum CooldownType { | ||
User = 'user', | ||
Guild = 'guild', | ||
Channel = 'channel', | ||
} | ||
|
||
export class Cooldowns extends BaseResource<CooldownData> { | ||
namespace = 'cooldowns'; | ||
|
||
filter(_data: CooldownData, _id: string): boolean { | ||
return true; | ||
} | ||
|
||
override set(id: string, data: CooldownDataInsert){ | ||
return super.set(id, {...data, lastDrip: getMilliseconds()}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
const { strict: assert } = require("node:assert/strict"); | ||
const { test, describe, beforeEach } = require('node:test'); | ||
const { Client, Cache, MemoryAdapter, Logger } = require('seyfert'); | ||
const { CommandHandler } = require('seyfert/lib/commands/handler.js'); | ||
const { CooldownManager } = require('../lib/manager.js'); | ||
const { CooldownType } = require('../lib/resource.js'); | ||
|
||
|
||
describe('CooldownManager', async () => { | ||
let client; | ||
let cooldownManager; | ||
|
||
beforeEach(() => { | ||
client = new Client(); | ||
|
||
const handler = new CommandHandler(new Logger({ active: true }), client); | ||
handler.values = [ | ||
// @ts-expect-error | ||
{ | ||
name: 'testCommand', | ||
cooldown: { | ||
type: CooldownType.User, | ||
interval: 1000, | ||
refill: 3, | ||
tokens: 1 | ||
} | ||
} | ||
] | ||
|
||
client.commands = handler; | ||
|
||
|
||
client.cache = new Cache(0, new MemoryAdapter(), {}, client); | ||
cooldownManager = new CooldownManager(client); | ||
client.cache.cooldown = cooldownManager.resource; | ||
}); | ||
|
||
await test('getData should return cooldown data for a command', () => { | ||
const data = cooldownManager.getData('testCommand'); | ||
assert.deepEqual(data, { | ||
type: CooldownType.User, | ||
interval: 1000, | ||
refill: 3, | ||
tokens: 1 | ||
}); | ||
}); | ||
|
||
await test('getData should return undefined for non-existent command', () => { | ||
const data = cooldownManager.getData('nonExistentCommand'); | ||
assert.equal(data, undefined); | ||
}); | ||
|
||
await test('has should return false for a new cooldown', () => { | ||
const result = cooldownManager.has('testCommand', 'user1'); | ||
assert.equal(result, false); | ||
}); | ||
|
||
await test('has should return true when cooldown is active', () => { | ||
cooldownManager.use('testCommand', 'user1', 1000); | ||
const result = cooldownManager.has('testCommand', 'user1'); | ||
assert.equal(result, true); | ||
}); | ||
|
||
await test('use should set cooldown when used for the first time', () => { | ||
const result = cooldownManager.use('testCommand', 'user1'); | ||
assert.equal(result, true); | ||
}); | ||
|
||
await test('use should return true when cooldown is active', () => { | ||
cooldownManager.use('testCommand', 'user1'); | ||
const result = cooldownManager.use('testCommand', 'user1'); | ||
assert.equal(result, true); | ||
}); | ||
|
||
await test('refill should refill the cooldown', () => { | ||
cooldownManager.use('testCommand', 'user1'); | ||
const result = cooldownManager.refill('testCommand', 'user1'); | ||
assert.equal(result, true); | ||
assert.equal(cooldownManager.has('testCommand', 'user1'), false); | ||
}); | ||
|
||
await test('drip should drip the cooldown over time', async () => { | ||
cooldownManager.use('testCommand', 'user1'); | ||
|
||
// Simulate time passing | ||
await new Promise(resolve => setTimeout(resolve, 1500)); // Half the interval | ||
|
||
const data = cooldownManager.resource.get('testCommand:user:user1'); | ||
const props = cooldownManager.getData('testCommand'); | ||
const result = cooldownManager.drip('testCommand', 'user1', props, data); | ||
assert.ok(result.remaining === 3); | ||
}); | ||
}); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
{ | ||
"compilerOptions": { | ||
"module": "CommonJS", | ||
"target": "ESNext", | ||
"lib": [ | ||
"ESNext", | ||
"WebWorker" | ||
], | ||
"moduleResolution": "node", | ||
"declaration": true, | ||
"sourceMap": false, | ||
"strict": true, | ||
"esModuleInterop": true, | ||
"experimentalDecorators": true, | ||
"emitDecoratorMetadata": true, | ||
"preserveConstEnums": true, | ||
/* Type Checking */ | ||
"noImplicitAny": true, | ||
"strictNullChecks": true, | ||
"strictFunctionTypes": true, | ||
"noImplicitThis": true, | ||
"noUnusedLocals": true, | ||
"noUnusedParameters": true, | ||
"noImplicitReturns": true, | ||
"noFallthroughCasesInSwitch": true, | ||
"skipLibCheck": true, | ||
"noErrorTruncation": true, | ||
"outDir": "./lib", | ||
"stripInternal": true, | ||
}, | ||
"exclude": [ | ||
"**/lib", | ||
"**/__test__" | ||
], | ||
"include": [ | ||
"src" | ||
] | ||
} |
Oops, something went wrong.