From 8b8cf9f0841ab0f85bebc93657a052522f60ea6a Mon Sep 17 00:00:00 2001 From: twlite <46562212+twlite@users.noreply.github.com> Date: Sun, 22 Jun 2025 19:33:16 +0545 Subject: [PATCH 1/3] feat: add useful stuff --- .../14-useful-utilities/01-ratelimit.mdx | 118 ++++++ .../guide/14-useful-utilities/02-mutex.mdx | 145 +++++++ .../14-useful-utilities/03-semaphore.mdx | 170 ++++++++ .../14-useful-utilities/04-async-queue.mdx | 199 ++++++++++ .../docs/guide/14-useful-utilities/index.mdx | 177 +++++++++ packages/commandkit/async-queue.cjs | 9 + packages/commandkit/async-queue.d.ts | 1 + packages/commandkit/env.cjs | 13 + packages/commandkit/env.d.ts | 6 + packages/commandkit/mutex.cjs | 21 + packages/commandkit/mutex.d.ts | 1 + packages/commandkit/package.json | 39 +- packages/commandkit/ratelimit.cjs | 26 ++ packages/commandkit/ratelimit.d.ts | 1 + packages/commandkit/semaphore.cjs | 23 ++ packages/commandkit/semaphore.d.ts | 1 + .../src/utils/useful-stuff/async-queue.ts | 175 ++++++++ .../src/utils/useful-stuff/mutex.ts | 302 ++++++++++++++ .../src/utils/useful-stuff/ratelimiter.ts | 288 ++++++++++++++ .../src/utils/useful-stuff/semaphore.ts | 374 ++++++++++++++++++ 20 files changed, 2087 insertions(+), 2 deletions(-) create mode 100644 apps/website/docs/guide/14-useful-utilities/01-ratelimit.mdx create mode 100644 apps/website/docs/guide/14-useful-utilities/02-mutex.mdx create mode 100644 apps/website/docs/guide/14-useful-utilities/03-semaphore.mdx create mode 100644 apps/website/docs/guide/14-useful-utilities/04-async-queue.mdx create mode 100644 apps/website/docs/guide/14-useful-utilities/index.mdx create mode 100644 packages/commandkit/async-queue.cjs create mode 100644 packages/commandkit/async-queue.d.ts create mode 100644 packages/commandkit/env.cjs create mode 100644 packages/commandkit/env.d.ts create mode 100644 packages/commandkit/mutex.cjs create mode 100644 packages/commandkit/mutex.d.ts create mode 100644 packages/commandkit/ratelimit.cjs create mode 100644 packages/commandkit/ratelimit.d.ts create mode 100644 packages/commandkit/semaphore.cjs create mode 100644 packages/commandkit/semaphore.d.ts create mode 100644 packages/commandkit/src/utils/useful-stuff/async-queue.ts create mode 100644 packages/commandkit/src/utils/useful-stuff/mutex.ts create mode 100644 packages/commandkit/src/utils/useful-stuff/ratelimiter.ts create mode 100644 packages/commandkit/src/utils/useful-stuff/semaphore.ts diff --git a/apps/website/docs/guide/14-useful-utilities/01-ratelimit.mdx b/apps/website/docs/guide/14-useful-utilities/01-ratelimit.mdx new file mode 100644 index 00000000..b5bdaf80 --- /dev/null +++ b/apps/website/docs/guide/14-useful-utilities/01-ratelimit.mdx @@ -0,0 +1,118 @@ +--- +title: Rate Limiter +description: Control request frequency with configurable limits and intervals +--- + +# Rate Limiter + +Think of a rate limiter like a traffic light for your application. It controls how often something can happen - like how many times a user can click a button or make an API request. This prevents your system from being overwhelmed. + +## Basic Usage + +The simplest way to use rate limiting is with the default settings: + +```typescript +import { ratelimit } from 'commandkit/ratelimit'; + +// Check if this user can make another request +const allowed = await ratelimit('user:123'); +if (allowed) { + // Process the request + console.log('Request processed successfully'); +} else { + // User has made too many requests + console.log('Please wait before making another request'); +} +``` + +## Custom Configuration + +Sometimes you need different limits for different situations. You can create a custom rate limiter with specific settings: + +```typescript +import { createRateLimiter } from 'commandkit/ratelimit'; + +// Create a stricter rate limiter for API endpoints +const apiLimiter = createRateLimiter({ + maxRequests: 5, // Allow 5 requests + interval: 30000, // Per 30 seconds +}); + +const allowed = await apiLimiter.limit('api:endpoint'); +``` + +## Advanced Usage + +### Checking Remaining Requests + +You can check how many requests a user has left and when the limit resets: + +```typescript +import { getRemainingRequests, getResetTime } from 'commandkit/ratelimit'; + +const remaining = await getRemainingRequests('user:123'); +const resetTime = await getResetTime('user:123'); + +console.log(`${remaining} requests remaining`); +console.log(`Limit resets in ${Math.round(resetTime / 1000)} seconds`); +``` + +### Manual Reset + +In some cases, you might want to reset a user's rate limit (like after they upgrade their account): + +```typescript +import { resetRateLimit } from 'commandkit/ratelimit'; + +// Give the user a fresh start +await resetRateLimit('user:123'); +``` + +### Using External Storage + +By default, rate limiters store data in memory. If you're running multiple servers, you'll want to use external storage like Redis so all servers can share the same rate limit information: + +```typescript +import { RateLimiter, RateLimitStorage } from 'commandkit/ratelimit'; + +// Example of how you might use Redis for rate limiting +class RedisRateLimitStorage implements RateLimitStorage { + async get(key: string): Promise { + // Get the current count from Redis + return (await redis.get(key)) || 0; + } + + async set(key: string, value: number): Promise { + // Store the count in Redis + await redis.set(key, value); + } + + async delete(key: string): Promise { + // Remove the count from Redis + await redis.del(key); + } +} + +const limiter = new RateLimiter(10, 60000, new RedisRateLimitStorage()); +``` + +## Default Settings + +- **Max Requests**: 10 requests +- **Time Window**: 60 seconds (1 minute) +- **Storage**: In-memory (works for single-server applications) + +## Common Use Cases + +- **API Rate Limiting**: Prevent users from making too many API calls +- **User Action Throttling**: Limit how often users can click buttons or submit forms +- **Resource Access Control**: Control access to expensive operations +- **Spam Prevention**: Stop automated bots from overwhelming your system + +## Tips for Beginners + +1. **Start Simple**: Use the default `ratelimit()` function for basic needs +2. **Choose Good Keys**: Use descriptive keys like `user:123` or `api:endpoint` to make debugging easier +3. **Set Reasonable Limits**: Don't make limits too strict or too loose - find the right balance +4. **Handle Rejection**: Always check if the rate limit allows the action before proceeding +5. **Consider Your Users**: Think about legitimate use cases when setting limits diff --git a/apps/website/docs/guide/14-useful-utilities/02-mutex.mdx b/apps/website/docs/guide/14-useful-utilities/02-mutex.mdx new file mode 100644 index 00000000..a04b7000 --- /dev/null +++ b/apps/website/docs/guide/14-useful-utilities/02-mutex.mdx @@ -0,0 +1,145 @@ +--- +title: Mutex +description: Ensure exclusive access to shared resources with async mutex locks +--- + +# Mutex + +A mutex is like a "do not disturb" sign for your data. It ensures that only one operation can access a shared resource at a time. Imagine multiple people trying to edit the same document - a mutex makes sure only one person can edit it at once. + +## Basic Usage + +The easiest way to use a mutex is with the `withMutex` function, which automatically handles locking and unlocking: + +```typescript +import { withMutex } from 'commandkit/mutex'; + +// This ensures only one operation can update the shared resource at a time +const result = await withMutex('shared-resource', async () => { + // This code runs with exclusive access + return await updateSharedResource(); +}); +``` + +## Custom Configuration + +You can create a custom mutex with different timeout settings: + +```typescript +import { createMutex } from 'commandkit/mutex'; + +const mutex = createMutex({ + timeout: 60000, // 60 second timeout +}); + +const result = await mutex.withLock('resource', async () => { + return await criticalOperation(); +}); +``` + +## Advanced Usage + +### Manual Lock Management + +Sometimes you need more control over when locks are acquired and released: + +```typescript +import { acquireLock, releaseLock, isLocked } from 'commandkit/mutex'; + +// Acquire lock manually +const acquired = await acquireLock('resource', 30000); +if (acquired) { + try { + // Perform critical operation + await criticalOperation(); + } finally { + // Always release the lock, even if an error occurs + await releaseLock('resource'); + } +} + +// Check if resource is currently locked +const locked = await isLocked('resource'); +console.log(`Resource is ${locked ? 'locked' : 'available'}`); +``` + +### Cancelling Operations + +You can cancel a mutex operation if it takes too long or if you need to stop it for any reason: + +```typescript +import { withMutex } from 'commandkit/mutex'; + +// Create a timeout that cancels after 5 seconds +const signal = AbortSignal.timeout(5000); + +try { + const result = await withMutex( + 'resource', + async () => { + return await longRunningOperation(); + }, + 30000, + signal, + ); +} catch (error) { + if (error.message.includes('aborted')) { + console.log('Lock acquisition was cancelled'); + } +} +``` + +### Using External Storage + +By default, mutexes store lock information in memory. If you're running multiple servers, you'll want to use external storage like Redis: + +```typescript +import { Mutex, MutexStorage } from 'commandkit/mutex'; + +// Example of how you might use Redis for mutex locks +class RedisMutexStorage implements MutexStorage { + async acquire( + key: string, + timeout?: number, + signal?: AbortSignal, + ): Promise { + // Try to acquire a lock in Redis + // Return true if successful, false if already locked + } + + async release(key: string): Promise { + // Release the lock in Redis + } + + async isLocked(key: string): Promise { + // Check if a lock exists in Redis + } +} + +const mutex = new Mutex({ + timeout: 30000, + storage: new RedisMutexStorage(), +}); +``` + +## Default Settings + +- **Timeout**: 30 seconds (30000ms) +- **Storage**: In-memory (works for single-server applications) + +## Common Use Cases + +- **Database Transactions**: Ensure only one operation can modify data at a time +- **File System Access**: Prevent multiple operations from writing to the same file +- **Configuration Updates**: Make sure configuration changes don't conflict +- **Cache Invalidation**: Control when cache is cleared to prevent race conditions +- **Resource Pool Management**: Manage access to limited resources + +## Tips for Beginners + +1. **Use `withMutex` When Possible**: It automatically handles cleanup, so you don't forget to release locks +2. **Set Reasonable Timeouts**: Don't make timeouts too short (might fail unnecessarily) or too long (might hang forever) +3. **Use Descriptive Names**: Give your resources meaningful names like `user:123:profile` or `database:users` +4. **Handle Errors**: Always handle cases where lock acquisition fails +5. **Think About Deadlocks**: Be careful not to create situations where two operations wait for each other +6. **Consider Your Setup**: Use external storage if you have multiple servers diff --git a/apps/website/docs/guide/14-useful-utilities/03-semaphore.mdx b/apps/website/docs/guide/14-useful-utilities/03-semaphore.mdx new file mode 100644 index 00000000..e9aa6946 --- /dev/null +++ b/apps/website/docs/guide/14-useful-utilities/03-semaphore.mdx @@ -0,0 +1,170 @@ +--- +title: Semaphore +description: Control concurrent access to limited resources with async semaphores +--- + +# Semaphore + +A semaphore is like a parking lot with a limited number of spaces. It allows a specific number of operations to happen at the same time, but no more. Perfect for controlling access to limited resources like database connections. + +## Basic Usage + +The easiest way to use a semaphore is with the `withPermit` function, which automatically handles getting and releasing permits: + +```typescript +import { withPermit } from 'commandkit/semaphore'; + +// This ensures only a limited number of operations can run at once +const result = await withPermit('database-connection', async () => { + // This code runs with a permit from the semaphore + return await executeDatabaseQuery(); +}); +``` + +## Custom Configuration + +You can create a semaphore with specific limits and timeout settings: + +```typescript +import { createSemaphore } from 'commandkit/semaphore'; + +const semaphore = createSemaphore({ + permits: 5, // Allow 5 concurrent operations + timeout: 60000, // 60 second timeout +}); + +const result = await semaphore.withPermit('api-endpoint', async () => { + return await apiCall(); +}); +``` + +## Advanced Usage + +### Manual Permit Management + +Sometimes you need more control over when permits are acquired and released: + +```typescript +import { + acquirePermit, + releasePermit, + getAvailablePermits, +} from 'commandkit/semaphore'; + +// Acquire permit manually +const acquired = await acquirePermit('resource', 30000); +if (acquired) { + try { + // Perform operation with limited concurrency + await limitedOperation(); + } finally { + // Always release the permit, even if an error occurs + await releasePermit('resource'); + } +} + +// Check how many permits are available +const available = await getAvailablePermits('resource'); +console.log(`${available} permits available`); +``` + +### Cancelling Operations + +You can cancel a semaphore operation if it takes too long or if you need to stop it for any reason: + +```typescript +import { withPermit } from 'commandkit/semaphore'; + +// Create a timeout that cancels after 5 seconds +const signal = AbortSignal.timeout(5000); + +try { + const result = await withPermit( + 'resource', + async () => { + return await longRunningOperation(); + }, + 30000, + signal, + ); +} catch (error) { + if (error.message.includes('aborted')) { + console.log('Permit acquisition was cancelled'); + } +} +``` + +### Monitoring Semaphore State + +You can check how many permits are being used and how many are available: + +```typescript +import { getAvailablePermits, getAcquiredPermits } from 'commandkit/semaphore'; + +const available = await getAvailablePermits('database'); +const acquired = await getAcquiredPermits('database'); + +console.log(`Database connections: ${acquired} active, ${available} available`); +``` + +### Using External Storage + +By default, semaphores store permit information in memory. If you're running multiple servers, you'll want to use external storage like Redis: + +```typescript +import { Semaphore, SemaphoreStorage } from 'commandkit/semaphore'; + +// Example of how you might use Redis for semaphore permits +class RedisSemaphoreStorage implements SemaphoreStorage { + async acquire( + key: string, + timeout?: number, + signal?: AbortSignal, + ): Promise { + // Try to acquire a permit in Redis + // Return true if successful, false if no permits available + } + + async release(key: string): Promise { + // Release a permit in Redis + } + + async getAvailablePermits(key: string): Promise { + // Get the number of available permits from Redis + } + + async getTotalPermits(key: string): Promise { + // Get the total number of permits from Redis + } +} + +const semaphore = new Semaphore({ + permits: 10, + timeout: 30000, + storage: new RedisSemaphoreStorage(), +}); +``` + +## Default Settings + +- **Permits**: 1 (sequential access) +- **Timeout**: 30 seconds (30000ms) +- **Storage**: In-memory (works for single-server applications) + +## Common Use Cases + +- **Database Connection Pooling**: Limit how many database connections are used at once +- **API Rate Limiting with Concurrency**: Allow multiple API calls but not too many +- **File Upload Throttling**: Control how many files can be uploaded simultaneously +- **External Service Access**: Limit calls to third-party services +- **Resource Pool Management**: Manage access to limited resources like memory or CPU + +## Tips for Beginners + +1. **Use `withPermit` When Possible**: It automatically handles cleanup, so you don't forget to release permits +2. **Set Appropriate Limits**: Don't set too many permits (might overwhelm resources) or too few (might be too slow) +3. **Monitor Usage**: Keep an eye on how many permits are being used to optimize performance +4. **Use Descriptive Names**: Give your resources meaningful names like `database:main` or `api:external` +5. **Handle Errors**: Always handle cases where permit acquisition fails +6. **Consider Your Resources**: Set permit limits based on what your system can actually handle +7. **Think About Your Setup**: Use external storage if you have multiple servers diff --git a/apps/website/docs/guide/14-useful-utilities/04-async-queue.mdx b/apps/website/docs/guide/14-useful-utilities/04-async-queue.mdx new file mode 100644 index 00000000..950da684 --- /dev/null +++ b/apps/website/docs/guide/14-useful-utilities/04-async-queue.mdx @@ -0,0 +1,199 @@ +--- +title: Async Queue +description: Process tasks sequentially or with limited concurrency using async queues +--- + +# Async Queue + +An async queue is like a line at a bank. Tasks wait in line and get processed one by one (or a few at a time). This helps you control how many things happen simultaneously and ensures everything gets done in an orderly way. + +## Basic Usage + +The simplest way to use a queue is with the default sequential processing: + +```typescript +import { createAsyncQueue } from 'commandkit/async-queue'; + +// Create a sequential queue (default) +const queue = createAsyncQueue(); + +// Add tasks to the queue +const result1 = await queue.add(async () => { + return await processTask1(); +}); + +const result2 = await queue.add(async () => { + return await processTask2(); +}); +``` + +## Custom Configuration + +You can create a queue that processes multiple tasks at the same time: + +```typescript +import { createAsyncQueue } from 'commandkit/async-queue'; + +// Allow 3 concurrent tasks +const queue = createAsyncQueue({ + concurrency: 3, + onDrain: () => { + console.log('All tasks completed'); + }, +}); + +// Tasks will run with up to 3 concurrent executions +const promises = [ + queue.add(async () => await task1()), + queue.add(async () => await task2()), + queue.add(async () => await task3()), + queue.add(async () => await task4()), +]; + +const results = await Promise.all(promises); +``` + +## Advanced Usage + +### Queue Control + +You can pause and resume the queue, and check its status: + +```typescript +import { createAsyncQueue } from 'commandkit/async-queue'; + +const queue = createAsyncQueue({ concurrency: 2 }); + +// Pause the queue +queue.pause(); + +// Add tasks (they won't execute until resumed) +const promise1 = queue.add(async () => await task1()); +const promise2 = queue.add(async () => await task2()); + +// Resume the queue +queue.resume(); + +// Check queue status +console.log(`Running: ${queue.getRunning()}`); +console.log(`Pending: ${queue.getPending()}`); +console.log(`Paused: ${queue.isPaused()}`); +``` + +### Cancelling Operations + +You can cancel queue operations if they take too long or if you need to stop them: + +```typescript +import { createAsyncQueue } from 'commandkit/async-queue'; + +// Create a timeout that cancels after 5 seconds +const signal = AbortSignal.timeout(5000); + +const queue = createAsyncQueue({ + concurrency: 2, + signal, +}); + +// Add tasks with individual timeouts +const task1Promise = queue.add( + async () => await longRunningTask1(), + AbortSignal.timeout(3000), +); + +const task2Promise = queue.add( + async () => await longRunningTask2(), + AbortSignal.timeout(3000), +); + +try { + await Promise.all([task1Promise, task2Promise]); +} catch (error) { + if (error.message.includes('aborted')) { + console.log('Queue was aborted'); + } +} +``` + +### Batch Processing + +Queues are great for processing large batches of items efficiently: + +```typescript +import { createAsyncQueue } from 'commandkit/async-queue'; + +const queue = createAsyncQueue({ + concurrency: 5, + onDrain: () => console.log('Batch processing complete'), +}); + +// Process a large batch of items +const items = Array.from({ length: 100 }, (_, i) => i); + +const processItem = async (item: number) => { + await new Promise((resolve) => setTimeout(resolve, 100)); + return `Processed item ${item}`; +}; + +// Add all items to queue +const promises = items.map((item) => queue.add(() => processItem(item))); + +// Wait for all to complete +const results = await Promise.all(promises); +``` + +### Error Handling + +Queues handle errors gracefully, so one failed task doesn't stop the others: + +```typescript +import { createAsyncQueue } from 'commandkit/async-queue'; + +const queue = createAsyncQueue({ concurrency: 2 }); + +// Handle individual task errors +const results = await Promise.allSettled([ + queue.add(async () => { + throw new Error('Task failed'); + }), + queue.add(async () => { + return 'Task succeeded'; + }), + queue.add(async () => { + return 'Another success'; + }), +]); + +results.forEach((result, index) => { + if (result.status === 'fulfilled') { + console.log(`Task ${index}: ${result.value}`); + } else { + console.log(`Task ${index}: ${result.reason.message}`); + } +}); +``` + +## Default Settings + +- **Concurrency**: 1 (sequential processing) +- **Storage**: In-memory +- **Abort Support**: Yes + +## Common Use Cases + +- **API Rate Limiting**: Control how many API calls are made at once +- **File Processing Pipelines**: Process files one by one or in small batches +- **Database Operation Batching**: Execute database operations in controlled batches +- **Image Processing Queues**: Process images without overwhelming the system +- **Email Sending Queues**: Send emails in an orderly way +- **Background Job Processing**: Handle background tasks efficiently + +## Tips for Beginners + +1. **Start with Sequential**: Use the default concurrency of 1 for simple cases +2. **Set Reasonable Limits**: Don't set concurrency too high (might overwhelm resources) or too low (might be too slow) +3. **Use `onDrain` Callback**: Get notified when all tasks are complete +4. **Handle Errors**: Use `Promise.allSettled()` to handle individual task failures +5. **Monitor Queue State**: Check running and pending counts for debugging +6. **Use Timeouts**: Set timeouts to prevent tasks from hanging forever +7. **Think About Your Resources**: Set concurrency based on what your system can handle diff --git a/apps/website/docs/guide/14-useful-utilities/index.mdx b/apps/website/docs/guide/14-useful-utilities/index.mdx new file mode 100644 index 00000000..16851f56 --- /dev/null +++ b/apps/website/docs/guide/14-useful-utilities/index.mdx @@ -0,0 +1,177 @@ +--- +title: Useful Utilities +description: Essential utilities for async operations, concurrency control, and resource management +--- + +# Useful Utilities + +CommandKit provides a collection of essential utilities that help you manage common programming challenges like controlling how many requests can be made, ensuring only one operation accesses a resource at a time, and managing multiple tasks efficiently. + +## Available Utilities + +### [Rate Limiter](./01-ratelimit.mdx) + +Think of rate limiting like a traffic light for your application. It controls how often something can happen - like how many times a user can click a button or make an API request. This prevents your system from being overwhelmed. + +```typescript +import { ratelimit } from 'commandkit/ratelimit'; + +const allowed = await ratelimit('user:123'); +if (allowed) { + // Process the request +} +``` + +### [Mutex](./02-mutex.mdx) + +A mutex is like a "do not disturb" sign for your data. It ensures that only one operation can access a shared resource at a time. Imagine multiple people trying to edit the same document - a mutex makes sure only one person can edit it at once. + +```typescript +import { withMutex } from 'commandkit/mutex'; + +const result = await withMutex('shared-resource', async () => { + return await updateSharedResource(); +}); +``` + +### [Semaphore](./03-semaphore.mdx) + +A semaphore is like a parking lot with a limited number of spaces. It allows a specific number of operations to happen at the same time, but no more. Perfect for controlling access to limited resources like database connections. + +```typescript +import { withPermit } from 'commandkit/semaphore'; + +const result = await withPermit('database-connection', async () => { + return await executeDatabaseQuery(); +}); +``` + +### [Async Queue](./04-async-queue.mdx) + +An async queue is like a line at a bank. Tasks wait in line and get processed one by one (or a few at a time). This helps you control how many things happen simultaneously and ensures everything gets done in an orderly way. + +```typescript +import { createAsyncQueue } from 'commandkit/async-queue'; + +const queue = createAsyncQueue({ concurrency: 3 }); +const result = await queue.add(async () => await processTask()); +``` + +## Common Patterns + +### Cancelling Operations + +All utilities support cancellation, which is like having an emergency stop button. You can cancel operations if they take too long or if you need to stop them for any reason. + +```typescript +// Create a timeout that cancels after 5 seconds +const signal = AbortSignal.timeout(5000); + +// Rate limiting with timeout +const allowed = await ratelimit('user:123'); + +// Mutex with timeout +const result = await withMutex( + 'resource', + async () => { + return await criticalOperation(); + }, + 30000, + signal, +); + +// Semaphore with timeout +const result = await withPermit( + 'resource', + async () => { + return await limitedOperation(); + }, + 30000, + signal, +); + +// Queue with timeout +const queue = createAsyncQueue({ signal }); +const result = await queue.add(async () => await task()); +``` + +### Using External Storage + +By default, these utilities store their data in memory. But if you're running multiple instances of your application (like on different servers), you'll want to use external storage like Redis so they can share information. + +```typescript +// Example of how you might use Redis for rate limiting +class RedisRateLimitStorage implements RateLimitStorage { + async get(key: string): Promise { + // Get the current count from Redis + return (await redis.get(key)) || 0; + } + async set(key: string, value: number): Promise { + // Store the count in Redis + await redis.set(key, value); + } + async delete(key: string): Promise { + // Remove the count from Redis + await redis.del(key); + } +} + +const limiter = createRateLimiter({ + storage: new RedisRateLimitStorage(), +}); +``` + +### Handling Errors Gracefully + +When things go wrong, it's important to handle errors properly so your application doesn't crash and can recover gracefully. + +```typescript +try { + const result = await withMutex('resource', async () => { + return await criticalOperation(); + }); +} catch (error) { + if (error.message.includes('aborted')) { + console.log('Operation was cancelled'); + } else if (error.message.includes('timeout')) { + console.log('Operation took too long'); + } +} +``` + +## When to Use Each Utility + +| Utility | What it does | When to use it | +| ---------------- | -------------------------------------------- | ------------------------------------------------------ | +| **Rate Limiter** | Controls how often something can happen | API endpoints, user actions, preventing spam | +| **Mutex** | Ensures only one thing can access a resource | Database transactions, file operations, config updates | +| **Semaphore** | Limits how many things can happen at once | Database connections, API concurrency, resource pools | +| **Async Queue** | Processes tasks in an orderly way | Batch operations, file processing, email sending | + +## Best Practices + +1. **Choose the right tool**: Think about what you're trying to achieve: + + - Need to limit how often something happens? Use a rate limiter + - Need to ensure only one thing accesses a resource? Use a mutex + - Need to limit how many things happen at once? Use a semaphore + - Need to process tasks in order? Use a queue + +2. **Set reasonable timeouts**: Always set timeouts to prevent your application from hanging forever if something goes wrong. + +3. **Use descriptive names**: Give your resources meaningful names so you can easily debug issues later. + +4. **Handle errors**: Always handle errors properly so your application can recover from problems. + +5. **Consider your setup**: If you're running multiple instances of your app, use external storage like Redis. + +6. **Monitor usage**: Keep an eye on how these utilities are being used to optimize performance. + +## Next Steps + +Ready to learn more? Check out the detailed guides for each utility: + +- [Rate Limiter Guide](./01-ratelimit.mdx) +- [Mutex Guide](./02-mutex.mdx) +- [Semaphore Guide](./03-semaphore.mdx) +- [Async Queue Guide](./04-async-queue.mdx) diff --git a/packages/commandkit/async-queue.cjs b/packages/commandkit/async-queue.cjs new file mode 100644 index 00000000..8f721ba2 --- /dev/null +++ b/packages/commandkit/async-queue.cjs @@ -0,0 +1,9 @@ +const { + AsyncQueue, + createAsyncQueue, +} = require('./dist/utils/useful-stuff/async-queue.js'); + +module.exports = { + AsyncQueue, + createAsyncQueue, +}; \ No newline at end of file diff --git a/packages/commandkit/async-queue.d.ts b/packages/commandkit/async-queue.d.ts new file mode 100644 index 00000000..6265ffd3 --- /dev/null +++ b/packages/commandkit/async-queue.d.ts @@ -0,0 +1 @@ +export * from './dist/utils/useful-stuff/async-queue.js'; diff --git a/packages/commandkit/env.cjs b/packages/commandkit/env.cjs new file mode 100644 index 00000000..982f2c85 --- /dev/null +++ b/packages/commandkit/env.cjs @@ -0,0 +1,13 @@ +const { + COMMANDKIT_BOOTSTRAP_MODE, + COMMANDKIT_IS_CLI, + COMMANDKIT_IS_DEV, + COMMANDKIT_IS_TEST, +} = require('commandkit'); + +module.exports = { + COMMANDKIT_BOOTSTRAP_MODE, + COMMANDKIT_IS_CLI, + COMMANDKIT_IS_DEV, + COMMANDKIT_IS_TEST, +}; diff --git a/packages/commandkit/env.d.ts b/packages/commandkit/env.d.ts new file mode 100644 index 00000000..a88d4c73 --- /dev/null +++ b/packages/commandkit/env.d.ts @@ -0,0 +1,6 @@ +export { + COMMANDKIT_BOOTSTRAP_MODE, + COMMANDKIT_IS_CLI, + COMMANDKIT_IS_DEV, + COMMANDKIT_IS_TEST, +} from 'commandkit'; diff --git a/packages/commandkit/mutex.cjs b/packages/commandkit/mutex.cjs new file mode 100644 index 00000000..b3f74534 --- /dev/null +++ b/packages/commandkit/mutex.cjs @@ -0,0 +1,21 @@ +const { + MemoryMutexStorage, + Mutex, + acquireLock, + createMutex, + defaultMutex, + isLocked, + releaseLock, + withMutex, +} = require('./dist/utils/useful-stuff/mutex.js'); + +module.exports = { + MemoryMutexStorage, + Mutex, + acquireLock, + createMutex, + defaultMutex, + isLocked, + releaseLock, + withMutex, +}; \ No newline at end of file diff --git a/packages/commandkit/mutex.d.ts b/packages/commandkit/mutex.d.ts new file mode 100644 index 00000000..5dff8a99 --- /dev/null +++ b/packages/commandkit/mutex.d.ts @@ -0,0 +1 @@ +export * from './dist/utils/useful-stuff/mutex.js'; diff --git a/packages/commandkit/package.json b/packages/commandkit/package.json index 7c5e353f..101a7f26 100644 --- a/packages/commandkit/package.json +++ b/packages/commandkit/package.json @@ -34,7 +34,17 @@ "./analytics.cjs", "./analytics.d.ts", "./ai.cjs", - "./ai.d.ts" + "./ai.d.ts", + "./env.cjs", + "./env.d.ts", + "./async-queue.cjs", + "./async-queue.d.ts", + "./ratelimit.cjs", + "./ratelimit.d.ts", + "./semaphore.cjs", + "./semaphore.d.ts", + "./mutex.cjs", + "./mutex.d.ts" ], "exports": { ".": { @@ -101,6 +111,31 @@ "require": "./ai.cjs", "import": "./ai.cjs", "types": "./ai.d.ts" + }, + "./env": { + "require": "./env.cjs", + "import": "./env.cjs", + "types": "./env.d.ts" + }, + "./async-queue": { + "require": "./async-queue.cjs", + "import": "./async-queue.cjs", + "types": "./async-queue.d.ts" + }, + "./ratelimit": { + "require": "./ratelimit.cjs", + "import": "./ratelimit.cjs", + "types": "./ratelimit.d.ts" + }, + "./semaphore": { + "require": "./semaphore.cjs", + "import": "./semaphore.cjs", + "types": "./semaphore.d.ts" + }, + "./mutex": { + "require": "./mutex.cjs", + "import": "./mutex.cjs", + "types": "./mutex.d.ts" } }, "scripts": { @@ -151,4 +186,4 @@ "engines": { "node": ">=22" } -} +} \ No newline at end of file diff --git a/packages/commandkit/ratelimit.cjs b/packages/commandkit/ratelimit.cjs new file mode 100644 index 00000000..e034a001 --- /dev/null +++ b/packages/commandkit/ratelimit.cjs @@ -0,0 +1,26 @@ +const { + DEFAULT_MAX_REQUESTS, + DEFAULT_TIMEOUT, + MemoryRateLimitStorage, + ratelimit, + RateLimiter, + createRateLimiter, + defaultRateLimiter, + getRemainingRequests, + getResetTime, + resetRateLimit, +} = require('./dist/utils/useful-stuff/ratelimiter.js'); + +module.exports = { + RateLimiter, + createRateLimiter, + defaultRateLimiter, + getRemainingRequests, + getResetTime, + resetRateLimit, + RateLimitStorage, + DEFAULT_MAX_REQUESTS, + DEFAULT_TIMEOUT, + MemoryRateLimitStorage, + ratelimit, +}; \ No newline at end of file diff --git a/packages/commandkit/ratelimit.d.ts b/packages/commandkit/ratelimit.d.ts new file mode 100644 index 00000000..458ef2aa --- /dev/null +++ b/packages/commandkit/ratelimit.d.ts @@ -0,0 +1 @@ +export * from './dist/utils/useful-stuff/ratelimiter.js'; diff --git a/packages/commandkit/semaphore.cjs b/packages/commandkit/semaphore.cjs new file mode 100644 index 00000000..36c7cca4 --- /dev/null +++ b/packages/commandkit/semaphore.cjs @@ -0,0 +1,23 @@ +const { + MemorySemaphoreStorage, + Semaphore, + acquirePermit, + createSemaphore, + defaultSemaphore, + getAcquiredPermits, + getAvailablePermits, + releasePermit, + withPermit, +} = require('./dist/utils/useful-stuff/semaphore.js'); + +module.exports = { + MemorySemaphoreStorage, + Semaphore, + acquirePermit, + createSemaphore, + defaultSemaphore, + getAcquiredPermits, + getAvailablePermits, + releasePermit, + withPermit, +}; \ No newline at end of file diff --git a/packages/commandkit/semaphore.d.ts b/packages/commandkit/semaphore.d.ts new file mode 100644 index 00000000..9d30d6ff --- /dev/null +++ b/packages/commandkit/semaphore.d.ts @@ -0,0 +1 @@ +export * from './dist/utils/useful-stuff/semaphore.js'; diff --git a/packages/commandkit/src/utils/useful-stuff/async-queue.ts b/packages/commandkit/src/utils/useful-stuff/async-queue.ts new file mode 100644 index 00000000..e2dbf086 --- /dev/null +++ b/packages/commandkit/src/utils/useful-stuff/async-queue.ts @@ -0,0 +1,175 @@ +/** + * Async queue implementation for processing tasks sequentially or with limited concurrency. + * Useful for rate-limiting, batching, or controlling resource usage. + */ + +export interface AsyncQueueOptions { + /** Maximum number of concurrent tasks. Default: 1 (sequential) */ + concurrency?: number; + /** Optional callback invoked when all tasks are completed */ + onDrain?: () => void | Promise; + /** Optional AbortSignal for cancelling the queue */ + signal?: AbortSignal; +} + +export type AsyncQueueTask = () => Promise; + +export class AsyncQueue { + private readonly concurrency: number; + private readonly onDrain?: () => void | Promise; + private readonly signal?: AbortSignal; + private running = 0; + private paused = false; + private aborted = false; + private queue: Array<() => void> = []; + + constructor(options: AsyncQueueOptions = {}) { + this.concurrency = options.concurrency ?? 1; + this.onDrain = options.onDrain; + this.signal = options.signal; + + if (this.signal) { + this.signal.addEventListener('abort', () => { + this.abort(); + }); + } + } + + /** + * Adds a task to the queue. + * @param task - The async function to execute. + * @param signal - Optional AbortSignal for cancelling this specific task. + * @returns Promise resolving to the result of the task. + */ + public add(task: AsyncQueueTask, signal?: AbortSignal): Promise { + if (this.aborted) { + return Promise.reject(new Error('Queue has been aborted')); + } + + return new Promise((resolve, reject) => { + const run = async () => { + if (this.paused || this.aborted) { + if (this.aborted) { + reject(new Error('Queue has been aborted')); + } else { + this.queue.push(run); + } + return; + } + + // Check if task-specific signal is aborted + if (signal?.aborted) { + reject(new Error('Task was aborted')); + return; + } + + this.running++; + try { + const result = await task(); + resolve(result); + } catch (err) { + reject(err); + } finally { + this.running--; + this.next(); + } + }; + + if (this.running < this.concurrency && !this.paused && !this.aborted) { + run(); + } else { + this.queue.push(run); + } + }); + } + + /** + * Pauses the queue. No new tasks will be started until resumed. + */ + public pause() { + this.paused = true; + } + + /** + * Resumes the queue and processes pending tasks. + */ + public resume() { + if (!this.paused) return; + this.paused = false; + this.next(); + } + + /** + * Aborts the queue, rejecting all pending tasks. + */ + public abort() { + this.aborted = true; + this.clear(); + } + + /** + * Clears all pending tasks from the queue. + */ + public clear() { + this.queue = []; + } + + /** + * Returns the number of running tasks. + */ + public getRunning(): number { + return this.running; + } + + /** + * Returns the number of pending tasks in the queue. + */ + public getPending(): number { + return this.queue.length; + } + + /** + * Returns true if the queue is currently paused. + */ + public isPaused(): boolean { + return this.paused; + } + + /** + * Returns true if the queue has been aborted. + */ + public isAborted(): boolean { + return this.aborted; + } + + private next() { + if (this.paused || this.aborted) return; + while (this.running < this.concurrency && this.queue.length > 0) { + const fn = this.queue.shift(); + if (fn) fn(); + } + if (this.running === 0 && this.queue.length === 0 && this.onDrain) { + this.onDrain(); + } + } +} + +/** + * Creates a new async queue instance with the specified configuration. + * @param options - Configuration options for the async queue. + * @returns New AsyncQueue instance. + * + * @example + * ```typescript + * const controller = new AbortController(); + * const queue = createAsyncQueue({ + * concurrency: 2, + * signal: controller.signal + * }); + * queue.add(async () => await doSomething()); + * controller.abort(); // Aborts the queue + * ``` + */ +export function createAsyncQueue(options?: AsyncQueueOptions): AsyncQueue { + return new AsyncQueue(options); +} diff --git a/packages/commandkit/src/utils/useful-stuff/mutex.ts b/packages/commandkit/src/utils/useful-stuff/mutex.ts new file mode 100644 index 00000000..b9ed4e8b --- /dev/null +++ b/packages/commandkit/src/utils/useful-stuff/mutex.ts @@ -0,0 +1,302 @@ +/** + * Async mutex implementation for coordinating access to shared resources. + * Provides mutual exclusion to ensure only one task can access a resource at a time. + */ + +/** + * Interface for mutex storage implementations. + * Provides methods to store, retrieve, and delete mutex lock data. + */ +export interface MutexStorage { + /** + * Attempts to acquire a lock for a given key + * @param key - The unique identifier for the mutex lock + * @param timeout - Optional timeout in milliseconds for the lock + * @param signal - Optional AbortSignal for cancelling the acquisition + * @returns Promise resolving to true if lock was acquired, false if timeout or already locked + */ + acquire( + key: string, + timeout?: number, + signal?: AbortSignal, + ): Promise; + + /** + * Releases the lock for a given key + * @param key - The unique identifier for the mutex lock + * @returns Promise that resolves when the lock is released + */ + release(key: string): Promise; + + /** + * Checks if a lock is currently held for a given key + * @param key - The unique identifier for the mutex lock + * @returns Promise resolving to true if the lock is held, false otherwise + */ + isLocked(key: string): Promise; +} + +/** + * Configuration options for mutex + */ +export interface MutexOptions { + /** Default timeout in milliseconds for lock acquisition. Default: 30000 */ + timeout?: number; + /** Storage implementation for persisting mutex data. Default: {@link MemoryMutexStorage} */ + storage?: MutexStorage; +} + +/** + * In-memory storage implementation for mutex locks. + * Suitable for single-instance applications. + */ +export class MemoryMutexStorage implements MutexStorage { + private readonly locks = new Map< + string, + { holder: string; acquiredAt: number } + >(); + + /** + * Attempts to acquire a lock for a given key + * @param key - The unique identifier for the mutex lock + * @param timeout - Optional timeout in milliseconds for the lock + * @param signal - Optional AbortSignal for cancelling the acquisition + * @returns Promise resolving to true if lock was acquired, false if timeout or already locked + */ + async acquire( + key: string, + timeout: number = 30000, + signal?: AbortSignal, + ): Promise { + const holder = this.generateHolderId(); + const startTime = Date.now(); + + while (Date.now() - startTime < timeout) { + // Check if aborted + if (signal?.aborted) { + throw new Error('Lock acquisition was aborted'); + } + + if (!this.locks.has(key)) { + this.locks.set(key, { holder, acquiredAt: Date.now() }); + return true; + } + await this.delay(10); + } + + return false; + } + + /** + * Releases the lock for a given key + * @param key - The unique identifier for the mutex lock + * @returns Promise that resolves when the lock is released + */ + async release(key: string): Promise { + this.locks.delete(key); + } + + /** + * Checks if a lock is currently held for a given key + * @param key - The unique identifier for the mutex lock + * @returns Promise resolving to true if the lock is held, false otherwise + */ + async isLocked(key: string): Promise { + return this.locks.has(key); + } + + private generateHolderId(): string { + return `holder_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + + private delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } +} + +/** + * Async mutex implementation that provides mutual exclusion for shared resources. + * Ensures only one task can access a protected resource at a time. + */ +export class Mutex { + private storage: MutexStorage; + private readonly defaultTimeout: number; + + /** + * Creates a new mutex instance + * @param options - Configuration options for the mutex + */ + public constructor(options: MutexOptions = {}) { + this.storage = options.storage || new MemoryMutexStorage(); + this.defaultTimeout = options.timeout || 30000; + } + + /** + * Sets the storage implementation for the mutex + * @param storage - The storage implementation to use + */ + public setStorage(storage: MutexStorage) { + this.storage = storage; + } + + /** + * Gets the storage implementation for the mutex + * @returns The storage implementation + */ + public getStorage(): MutexStorage { + return this.storage; + } + + /** + * Acquires a lock for the given key + * @param key - The unique identifier for the mutex lock + * @param timeout - Optional timeout in milliseconds for lock acquisition + * @param signal - Optional AbortSignal for cancelling the acquisition + * @returns Promise resolving to true if lock was acquired, false if timeout + */ + public async acquire( + key: string, + timeout?: number, + signal?: AbortSignal, + ): Promise { + return this.storage.acquire(key, timeout || this.defaultTimeout, signal); + } + + /** + * Releases the lock for the given key + * @param key - The unique identifier for the mutex lock + * @returns Promise that resolves when the lock is released + */ + public async release(key: string): Promise { + return this.storage.release(key); + } + + /** + * Checks if a lock is currently held for the given key + * @param key - The unique identifier for the mutex lock + * @returns Promise resolving to true if the lock is held, false otherwise + */ + public async isLocked(key: string): Promise { + return this.storage.isLocked(key); + } + + /** + * Executes a function with exclusive access to the resource + * @param key - The unique identifier for the mutex lock + * @param fn - The function to execute with exclusive access + * @param timeout - Optional timeout in milliseconds for lock acquisition + * @param signal - Optional AbortSignal for cancelling the lock acquisition + * @returns Promise resolving to the result of the function execution + * @throws Error if lock acquisition fails or function execution fails + */ + public async withLock( + key: string, + fn: () => Promise | T, + timeout?: number, + signal?: AbortSignal, + ): Promise { + const acquired = await this.acquire(key, timeout, signal); + if (!acquired) { + throw new Error(`Failed to acquire lock for key: ${key}`); + } + + try { + return await fn(); + } finally { + await this.release(key); + } + } + + /** + * Gets the current configuration of the mutex + * @returns Object containing the current timeout value + */ + public getConfig(): Omit { + return { + timeout: this.defaultTimeout, + }; + } +} + +/** + * Default mutex instance for global use + */ +export const defaultMutex = new Mutex(); + +/** + * Convenience function to execute a function with exclusive access using the default mutex. + * + * @param key - The unique identifier for the mutex lock + * @param fn - The function to execute with exclusive access + * @param timeout - Optional timeout in milliseconds for lock acquisition + * @param signal - Optional AbortSignal for cancelling the lock acquisition + * @returns Promise resolving to the result of the function execution + * + * @example + * ```typescript + * const controller = new AbortController(); + * const result = await withMutex('shared-resource', async () => { + * // This code runs with exclusive access + * return await updateSharedResource(); + * }, 30000, controller.signal); + * controller.abort(); // Cancels the lock acquisition + * ``` + */ +export async function withMutex( + key: string, + fn: () => Promise | T, + timeout?: number, + signal?: AbortSignal, +): Promise { + return defaultMutex.withLock(key, fn, timeout, signal); +} + +/** + * Creates a new mutex instance with the specified configuration + * @param options - Configuration options for the mutex + * @returns New Mutex instance + * + * @example + * ```typescript + * const mutex = createMutex({ + * timeout: 60000, + * storage: new RedisMutexStorage() + * }); + * ``` + */ +export function createMutex(options: MutexOptions): Mutex { + return new Mutex(options); +} + +/** + * Acquires a lock using the default mutex + * @param key - The unique identifier for the mutex lock + * @param timeout - Optional timeout in milliseconds for lock acquisition + * @param signal - Optional AbortSignal for cancelling the acquisition + * @returns Promise resolving to true if lock was acquired, false if timeout + */ +export async function acquireLock( + key: string, + timeout?: number, + signal?: AbortSignal, +): Promise { + return defaultMutex.acquire(key, timeout, signal); +} + +/** + * Releases a lock using the default mutex + * @param key - The unique identifier for the mutex lock + * @returns Promise that resolves when the lock is released + */ +export async function releaseLock(key: string): Promise { + return defaultMutex.release(key); +} + +/** + * Checks if a lock is held using the default mutex + * @param key - The unique identifier for the mutex lock + * @returns Promise resolving to true if the lock is held, false otherwise + */ +export async function isLocked(key: string): Promise { + return defaultMutex.isLocked(key); +} diff --git a/packages/commandkit/src/utils/useful-stuff/ratelimiter.ts b/packages/commandkit/src/utils/useful-stuff/ratelimiter.ts new file mode 100644 index 00000000..4453e715 --- /dev/null +++ b/packages/commandkit/src/utils/useful-stuff/ratelimiter.ts @@ -0,0 +1,288 @@ +/** + * Default timeout interval for rate limiting in milliseconds + */ +export const DEFAULT_TIMEOUT = 60000; + +/** + * Default maximum number of requests allowed per interval + */ +export const DEFAULT_MAX_REQUESTS = 10; + +/** + * Interface for rate limit storage implementations. + * Provides methods to store, retrieve, and delete rate limit data. + */ +export interface RateLimitStorage { + /** + * Retrieves the current request count for a given key + * @param key - The unique identifier for the rate limit entry + * @returns Promise resolving to the current request count + */ + get(key: string): Promise; + + /** + * Sets the request count for a given key + * @param key - The unique identifier for the rate limit entry + * @param value - The request count to store + * @returns Promise that resolves when the value is stored + */ + set(key: string, value: number): Promise; + + /** + * Deletes the rate limit entry for a given key + * @param key - The unique identifier for the rate limit entry + * @returns Promise that resolves when the entry is deleted + */ + delete(key: string): Promise; +} + +/** + * Configuration options for rate limiting + */ +export interface RateLimitOptions { + /** Maximum number of requests allowed per interval. Default: 10 */ + maxRequests?: number; + /** Time interval in milliseconds for the rate limit window. Default: 60000 */ + interval?: number; + /** Storage implementation for persisting rate limit data. Default: {@link MemoryRateLimitStorage} */ + storage?: RateLimitStorage; +} + +/** + * In-memory storage implementation for rate limiting. + * Suitable for single-instance applications. + */ +export class MemoryRateLimitStorage implements RateLimitStorage { + private readonly store = new Map(); + + /** + * Retrieves the current request count for a given key + * @param key - The unique identifier for the rate limit entry + * @returns Promise resolving to the current request count or 0 if not found + */ + async get(key: string): Promise { + return this.store.get(key) || 0; + } + + /** + * Sets the request count for a given key + * @param key - The unique identifier for the rate limit entry + * @param value - The request count to store + * @returns Promise that resolves immediately + */ + async set(key: string, value: number): Promise { + this.store.set(key, value); + } + + /** + * Deletes the rate limit entry for a given key + * @param key - The unique identifier for the rate limit entry + * @returns Promise that resolves immediately + */ + async delete(key: string): Promise { + this.store.delete(key); + } +} + +/** + * Rate limiter implementation that enforces request limits per key. + * Supports configurable request limits and time intervals. + */ +export class RateLimiter { + private readonly limits: Map = new Map(); + private readonly lastReset: Map = new Map(); + private readonly resetInterval: number; + + /** + * Creates a new rate limiter instance + * @param maxRequests - Maximum number of requests allowed per interval (default: 10) + * @param interval - Time interval in milliseconds for the rate limit window (default: 60000) + * @param storage - Optional storage implementation (default: MemoryRateLimitStorage) + */ + public constructor( + private readonly maxRequests: number = DEFAULT_MAX_REQUESTS, + private readonly interval: number = DEFAULT_TIMEOUT, + private storage: RateLimitStorage = new MemoryRateLimitStorage(), + ) { + this.resetInterval = interval; + } + + /** + * Sets the storage implementation for the rate limiter + * @param storage - The storage implementation to use + */ + public setStorage(storage: RateLimitStorage) { + this.storage = storage; + } + + /** + * Gets the storage implementation for the rate limiter + * @returns The storage implementation + */ + public getStorage(): RateLimitStorage { + return this.storage; + } + + /** + * Checks if a request is allowed for the given key and increments the counter if allowed + * @param key - The unique identifier for the rate limit entry + * @returns Promise resolving to true if the request is allowed, false if rate limited + */ + public async limit(key: string): Promise { + const now = Date.now(); + const lastReset = this.lastReset.get(key) || 0; + const timeSinceReset = now - lastReset; + + // Reset counter if interval has passed + if (timeSinceReset > this.resetInterval) { + await this.storage.delete(key); + this.lastReset.set(key, now); + this.limits.set(key, 0); + } + + // Get current count from storage + const currentCount = await this.storage.get(key); + + // Check if limit exceeded + if (currentCount >= this.maxRequests) { + return false; + } + + // Increment counter + const newCount = currentCount + 1; + await this.storage.set(key, newCount); + this.limits.set(key, newCount); + + return true; + } + + /** + * Gets the remaining requests allowed for a given key + * @param key - The unique identifier for the rate limit entry + * @returns Promise resolving to the number of remaining requests + */ + public async getRemaining(key: string): Promise { + const currentCount = await this.storage.get(key); + return Math.max(0, this.maxRequests - currentCount); + } + + /** + * Gets the time until the rate limit resets for a given key + * @param key - The unique identifier for the rate limit entry + * @returns Promise resolving to the time in milliseconds until reset, or 0 if no limit is active + */ + public async getResetTime(key: string): Promise { + const lastReset = this.lastReset.get(key) || 0; + const now = Date.now(); + const timeSinceReset = now - lastReset; + + if (timeSinceReset >= this.resetInterval) { + return 0; + } + + return this.resetInterval - timeSinceReset; + } + + /** + * Resets the rate limit for a given key + * @param key - The unique identifier for the rate limit entry + * @returns Promise that resolves when the reset is complete + */ + public async reset(key: string): Promise { + await this.storage.delete(key); + this.limits.delete(key); + this.lastReset.delete(key); + } + + /** + * Gets the current configuration of the rate limiter + * @returns Object containing the current maxRequests and interval values + */ + public getConfig(): Omit { + return { + maxRequests: this.maxRequests, + interval: this.interval, + }; + } +} + +/** + * Default rate limiter instance for global use + */ +export const defaultRateLimiter = new RateLimiter(); + +/** + * Convenience function to check if a request is allowed for a given key. + * Uses the default rate limiter instance. + * + * @param key - The unique identifier for the rate limit entry + * @returns Promise resolving to true if the request is allowed, false if rate limited + * + * @example + * ```typescript + * const allowed = await ratelimit('user:123'); + * // update the default rate limiter config + * import { defaultRateLimiter } from 'commandkit/ratelimit'; + * + * // update max allowed requests + * defaultRateLimiter.setMaxRequests(10); + * + * // update the timeout interval + * defaultRateLimiter.setInterval(30000); + * + * // update the storage implementation + * defaultRateLimiter.setStorage(new RedisStorage()); + * ``` + */ +export async function ratelimit(key: string): Promise { + return defaultRateLimiter.limit(key); +} + +/** + * Creates a new rate limiter instance with the specified configuration + * @param options - Configuration options for the rate limiter + * @returns New RateLimiter instance + * + * @example + * ```typescript + * const limiter = createRateLimiter({ + * maxRequests: 5, + * interval: 30000, + * storage: new CustomStorage() + * }); + * ``` + */ +export function createRateLimiter(options: RateLimitOptions): RateLimiter { + return new RateLimiter( + options.maxRequests, + options.interval, + options.storage, + ); +} + +/** + * Gets the remaining requests for a key using the default rate limiter + * @param key - The unique identifier for the rate limit entry + * @returns Promise resolving to the number of remaining requests + */ +export async function getRemainingRequests(key: string): Promise { + return defaultRateLimiter.getRemaining(key); +} + +/** + * Gets the reset time for a key using the default rate limiter + * @param key - The unique identifier for the rate limit entry + * @returns Promise resolving to the time in milliseconds until reset + */ +export async function getResetTime(key: string): Promise { + return defaultRateLimiter.getResetTime(key); +} + +/** + * Resets the rate limit for a key using the default rate limiter + * @param key - The unique identifier for the rate limit entry + * @returns Promise that resolves when the reset is complete + */ +export async function resetRateLimit(key: string): Promise { + return defaultRateLimiter.reset(key); +} diff --git a/packages/commandkit/src/utils/useful-stuff/semaphore.ts b/packages/commandkit/src/utils/useful-stuff/semaphore.ts new file mode 100644 index 00000000..7faa880a --- /dev/null +++ b/packages/commandkit/src/utils/useful-stuff/semaphore.ts @@ -0,0 +1,374 @@ +/** + * Async semaphore implementation for controlling access to a limited pool of resources. + * Allows a specified number of concurrent operations while blocking additional requests. + */ + +/** + * Interface for semaphore storage implementations. + * Provides methods to store, retrieve, and manage semaphore permit data. + */ +export interface SemaphoreStorage { + /** + * Attempts to acquire a permit for a given key + * @param key - The unique identifier for the semaphore + * @param timeout - Optional timeout in milliseconds for permit acquisition + * @param signal - Optional AbortSignal for cancelling the acquisition + * @returns Promise resolving to true if permit was acquired, false if timeout or no permits available + */ + acquire( + key: string, + timeout?: number, + signal?: AbortSignal, + ): Promise; + + /** + * Releases a permit for a given key + * @param key - The unique identifier for the semaphore + * @returns Promise that resolves when the permit is released + */ + release(key: string): Promise; + + /** + * Gets the number of available permits for a given key + * @param key - The unique identifier for the semaphore + * @returns Promise resolving to the number of available permits + */ + getAvailablePermits(key: string): Promise; + + /** + * Gets the total number of permits for a given key + * @param key - The unique identifier for the semaphore + * @returns Promise resolving to the total number of permits + */ + getTotalPermits(key: string): Promise; +} + +/** + * Configuration options for semaphore + */ +export interface SemaphoreOptions { + /** Maximum number of concurrent permits allowed. Default: 1 */ + permits?: number; + /** Default timeout in milliseconds for permit acquisition. Default: 30000 */ + timeout?: number; + /** Storage implementation for persisting semaphore data. Default: {@link MemorySemaphoreStorage} */ + storage?: SemaphoreStorage; +} + +/** + * In-memory storage implementation for semaphore permits. + * Suitable for single-instance applications. + */ +export class MemorySemaphoreStorage implements SemaphoreStorage { + private readonly semaphores = new Map< + string, + { total: number; available: number } + >(); + + /** + * Attempts to acquire a permit for a given key + * @param key - The unique identifier for the semaphore + * @param timeout - Optional timeout in milliseconds for permit acquisition + * @param signal - Optional AbortSignal for cancelling the acquisition + * @returns Promise resolving to true if permit was acquired, false if timeout or no permits available + */ + async acquire( + key: string, + timeout: number = 30000, + signal?: AbortSignal, + ): Promise { + const startTime = Date.now(); + + while (Date.now() - startTime < timeout) { + // Check if aborted + if (signal?.aborted) { + throw new Error('Permit acquisition was aborted'); + } + + const semaphore = this.semaphores.get(key); + if (semaphore && semaphore.available > 0) { + semaphore.available--; + return true; + } + await this.delay(10); + } + + return false; + } + + /** + * Releases a permit for a given key + * @param key - The unique identifier for the semaphore + * @returns Promise that resolves when the permit is released + */ + async release(key: string): Promise { + const semaphore = this.semaphores.get(key); + if (semaphore && semaphore.available < semaphore.total) { + semaphore.available++; + } + } + + /** + * Gets the number of available permits for a given key + * @param key - The unique identifier for the semaphore + * @returns Promise resolving to the number of available permits + */ + async getAvailablePermits(key: string): Promise { + const semaphore = this.semaphores.get(key); + return semaphore ? semaphore.available : 0; + } + + /** + * Gets the total number of permits for a given key + * @param key - The unique identifier for the semaphore + * @returns Promise resolving to the total number of permits + */ + async getTotalPermits(key: string): Promise { + const semaphore = this.semaphores.get(key); + return semaphore ? semaphore.total : 0; + } + + /** + * Initializes a semaphore with the specified number of permits + * @param key - The unique identifier for the semaphore + * @param permits - The total number of permits to allocate + */ + initialize(key: string, permits: number): void { + this.semaphores.set(key, { total: permits, available: permits }); + } + + private delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } +} + +/** + * Async semaphore implementation that controls access to a limited pool of resources. + * Allows a specified number of concurrent operations while blocking additional requests. + */ +export class Semaphore { + private storage: SemaphoreStorage; + private readonly defaultPermits: number; + private readonly defaultTimeout: number; + + /** + * Creates a new semaphore instance + * @param options - Configuration options for the semaphore + */ + public constructor(options: SemaphoreOptions = {}) { + this.storage = options.storage || new MemorySemaphoreStorage(); + this.defaultPermits = options.permits || 1; + this.defaultTimeout = options.timeout || 30000; + } + + /** + * Sets the storage implementation for the semaphore + * @param storage - The storage implementation to use + */ + public setStorage(storage: SemaphoreStorage) { + this.storage = storage; + } + + /** + * Gets the storage implementation for the semaphore + * @returns The storage implementation + */ + public getStorage(): SemaphoreStorage { + return this.storage; + } + + /** + * Acquires a permit for the given key + * @param key - The unique identifier for the semaphore + * @param timeout - Optional timeout in milliseconds for permit acquisition + * @param signal - Optional AbortSignal for cancelling the acquisition + * @returns Promise resolving to true if permit was acquired, false if timeout + */ + public async acquire( + key: string, + timeout?: number, + signal?: AbortSignal, + ): Promise { + // Initialize semaphore if it doesn't exist + if (this.storage instanceof MemorySemaphoreStorage) { + const totalPermits = await this.storage.getTotalPermits(key); + if (totalPermits === 0) { + (this.storage as MemorySemaphoreStorage).initialize( + key, + this.defaultPermits, + ); + } + } + + return this.storage.acquire(key, timeout || this.defaultTimeout, signal); + } + + /** + * Releases a permit for the given key + * @param key - The unique identifier for the semaphore + * @returns Promise that resolves when the permit is released + */ + public async release(key: string): Promise { + return this.storage.release(key); + } + + /** + * Gets the number of available permits for the given key + * @param key - The unique identifier for the semaphore + * @returns Promise resolving to the number of available permits + */ + public async getAvailablePermits(key: string): Promise { + return this.storage.getAvailablePermits(key); + } + + /** + * Gets the total number of permits for the given key + * @param key - The unique identifier for the semaphore + * @returns Promise resolving to the total number of permits + */ + public async getTotalPermits(key: string): Promise { + return this.storage.getTotalPermits(key); + } + + /** + * Gets the number of acquired permits for the given key + * @param key - The unique identifier for the semaphore + * @returns Promise resolving to the number of acquired permits + */ + public async getAcquiredPermits(key: string): Promise { + const total = await this.getTotalPermits(key); + const available = await this.getAvailablePermits(key); + return total - available; + } + + /** + * Executes a function with a permit from the semaphore + * @param key - The unique identifier for the semaphore + * @param fn - The function to execute with a permit + * @param timeout - Optional timeout in milliseconds for permit acquisition + * @param signal - Optional AbortSignal for cancelling the permit acquisition + * @returns Promise resolving to the result of the function execution + * @throws Error if permit acquisition fails or function execution fails + */ + public async withPermit( + key: string, + fn: () => Promise | T, + timeout?: number, + signal?: AbortSignal, + ): Promise { + const acquired = await this.acquire(key, timeout, signal); + if (!acquired) { + throw new Error(`Failed to acquire permit for key: ${key}`); + } + + try { + return await fn(); + } finally { + await this.release(key); + } + } + + /** + * Gets the current configuration of the semaphore + * @returns Object containing the current permits and timeout values + */ + public getConfig(): Omit { + return { + permits: this.defaultPermits, + timeout: this.defaultTimeout, + }; + } +} + +/** + * Default semaphore instance for global use + */ +export const defaultSemaphore = new Semaphore(); + +/** + * Convenience function to execute a function with a permit using the default semaphore. + * + * @param key - The unique identifier for the semaphore + * @param fn - The function to execute with a permit + * @param timeout - Optional timeout in milliseconds for permit acquisition + * @param signal - Optional AbortSignal for cancelling the permit acquisition + * @returns Promise resolving to the result of the function execution + * + * @example + * ```typescript + * const controller = new AbortController(); + * const result = await withPermit('database-connection', async () => { + * // This code runs with a permit from the semaphore + * return await executeDatabaseQuery(); + * }, 30000, controller.signal); + * controller.abort(); // Cancels the permit acquisition + * ``` + */ +export async function withPermit( + key: string, + fn: () => Promise | T, + timeout?: number, + signal?: AbortSignal, +): Promise { + return defaultSemaphore.withPermit(key, fn, timeout, signal); +} + +/** + * Creates a new semaphore instance with the specified configuration + * @param options - Configuration options for the semaphore + * @returns New Semaphore instance + * + * @example + * ```typescript + * const semaphore = createSemaphore({ + * permits: 5, + * timeout: 60000, + * storage: new RedisSemaphoreStorage() + * }); + * ``` + */ +export function createSemaphore(options: SemaphoreOptions): Semaphore { + return new Semaphore(options); +} + +/** + * Acquires a permit using the default semaphore + * @param key - The unique identifier for the semaphore + * @param timeout - Optional timeout in milliseconds for permit acquisition + * @param signal - Optional AbortSignal for cancelling the acquisition + * @returns Promise resolving to true if permit was acquired, false if timeout + */ +export async function acquirePermit( + key: string, + timeout?: number, + signal?: AbortSignal, +): Promise { + return defaultSemaphore.acquire(key, timeout, signal); +} + +/** + * Releases a permit using the default semaphore + * @param key - The unique identifier for the semaphore + * @returns Promise that resolves when the permit is released + */ +export async function releasePermit(key: string): Promise { + return defaultSemaphore.release(key); +} + +/** + * Gets the number of available permits using the default semaphore + * @param key - The unique identifier for the semaphore + * @returns Promise resolving to the number of available permits + */ +export async function getAvailablePermits(key: string): Promise { + return defaultSemaphore.getAvailablePermits(key); +} + +/** + * Gets the number of acquired permits using the default semaphore + * @param key - The unique identifier for the semaphore + * @returns Promise resolving to the number of acquired permits + */ +export async function getAcquiredPermits(key: string): Promise { + return defaultSemaphore.getAcquiredPermits(key); +} From 6f939c2fe01865f59bf4c5ea40847c209eb1627e Mon Sep 17 00:00:00 2001 From: twlite <46562212+twlite@users.noreply.github.com> Date: Sun, 22 Jun 2025 19:43:15 +0545 Subject: [PATCH 2/3] feat: implement Redis-based storage for rate limiting, mutex, and semaphore utilities --- .../14-useful-utilities/01-ratelimit.mdx | 37 ++-- .../guide/14-useful-utilities/02-mutex.mdx | 38 ++-- .../14-useful-utilities/03-semaphore.mdx | 43 ++-- .../docs/guide/14-useful-utilities/index.mdx | 41 ++-- packages/redis/README.md | 123 +++++++++++ packages/redis/src/index.ts | 4 + packages/redis/src/mutex-storage.ts | 126 +++++++++++ packages/redis/src/ratelimit-storage.ts | 18 ++ packages/redis/src/semaphore-storage.ts | 196 ++++++++++++++++++ 9 files changed, 546 insertions(+), 80 deletions(-) create mode 100644 packages/redis/src/mutex-storage.ts create mode 100644 packages/redis/src/ratelimit-storage.ts create mode 100644 packages/redis/src/semaphore-storage.ts diff --git a/apps/website/docs/guide/14-useful-utilities/01-ratelimit.mdx b/apps/website/docs/guide/14-useful-utilities/01-ratelimit.mdx index b5bdaf80..4424da63 100644 --- a/apps/website/docs/guide/14-useful-utilities/01-ratelimit.mdx +++ b/apps/website/docs/guide/14-useful-utilities/01-ratelimit.mdx @@ -74,26 +74,27 @@ By default, rate limiters store data in memory. If you're running multiple serve ```typescript import { RateLimiter, RateLimitStorage } from 'commandkit/ratelimit'; +import { RedisRateLimitStorage } from '@commandkit/redis'; +import { Redis } from 'ioredis'; -// Example of how you might use Redis for rate limiting -class RedisRateLimitStorage implements RateLimitStorage { - async get(key: string): Promise { - // Get the current count from Redis - return (await redis.get(key)) || 0; - } - - async set(key: string, value: number): Promise { - // Store the count in Redis - await redis.set(key, value); - } - - async delete(key: string): Promise { - // Remove the count from Redis - await redis.del(key); - } -} +// Create Redis client +const redis = new Redis(); + +// Use Redis-based rate limit storage +const limiter = new RateLimiter(10, 60000, new RedisRateLimitStorage(redis)); +``` + +You can also use the convenience function: -const limiter = new RateLimiter(10, 60000, new RedisRateLimitStorage()); +```typescript +import { createRateLimiter } from 'commandkit/ratelimit'; +import { RedisRateLimitStorage } from '@commandkit/redis'; + +const limiter = createRateLimiter({ + maxRequests: 10, + interval: 60000, + storage: new RedisRateLimitStorage(redis), +}); ``` ## Default Settings diff --git a/apps/website/docs/guide/14-useful-utilities/02-mutex.mdx b/apps/website/docs/guide/14-useful-utilities/02-mutex.mdx index a04b7000..1f80fcf3 100644 --- a/apps/website/docs/guide/14-useful-utilities/02-mutex.mdx +++ b/apps/website/docs/guide/14-useful-utilities/02-mutex.mdx @@ -95,30 +95,28 @@ By default, mutexes store lock information in memory. If you're running multiple ```typescript import { Mutex, MutexStorage } from 'commandkit/mutex'; +import { RedisMutexStorage } from '@commandkit/redis'; +import { Redis } from 'ioredis'; -// Example of how you might use Redis for mutex locks -class RedisMutexStorage implements MutexStorage { - async acquire( - key: string, - timeout?: number, - signal?: AbortSignal, - ): Promise { - // Try to acquire a lock in Redis - // Return true if successful, false if already locked - } - - async release(key: string): Promise { - // Release the lock in Redis - } - - async isLocked(key: string): Promise { - // Check if a lock exists in Redis - } -} +// Create Redis client +const redis = new Redis(); +// Use Redis-based mutex storage const mutex = new Mutex({ timeout: 30000, - storage: new RedisMutexStorage(), + storage: new RedisMutexStorage(redis), +}); +``` + +You can also use the convenience function: + +```typescript +import { createMutex } from 'commandkit/mutex'; +import { RedisMutexStorage } from '@commandkit/redis'; + +const mutex = createMutex({ + timeout: 60000, + storage: new RedisMutexStorage(redis), }); ``` diff --git a/apps/website/docs/guide/14-useful-utilities/03-semaphore.mdx b/apps/website/docs/guide/14-useful-utilities/03-semaphore.mdx index e9aa6946..d2249f60 100644 --- a/apps/website/docs/guide/14-useful-utilities/03-semaphore.mdx +++ b/apps/website/docs/guide/14-useful-utilities/03-semaphore.mdx @@ -113,35 +113,30 @@ By default, semaphores store permit information in memory. If you're running mul ```typescript import { Semaphore, SemaphoreStorage } from 'commandkit/semaphore'; +import { RedisSemaphoreStorage } from '@commandkit/redis'; +import { Redis } from 'ioredis'; -// Example of how you might use Redis for semaphore permits -class RedisSemaphoreStorage implements SemaphoreStorage { - async acquire( - key: string, - timeout?: number, - signal?: AbortSignal, - ): Promise { - // Try to acquire a permit in Redis - // Return true if successful, false if no permits available - } - - async release(key: string): Promise { - // Release a permit in Redis - } - - async getAvailablePermits(key: string): Promise { - // Get the number of available permits from Redis - } - - async getTotalPermits(key: string): Promise { - // Get the total number of permits from Redis - } -} +// Create Redis client +const redis = new Redis(); +// Use Redis-based semaphore storage const semaphore = new Semaphore({ permits: 10, timeout: 30000, - storage: new RedisSemaphoreStorage(), + storage: new RedisSemaphoreStorage(redis), +}); +``` + +You can also use the convenience function: + +```typescript +import { createSemaphore } from 'commandkit/semaphore'; +import { RedisSemaphoreStorage } from '@commandkit/redis'; + +const semaphore = createSemaphore({ + permits: 5, + timeout: 60000, + storage: new RedisSemaphoreStorage(redis), }); ``` diff --git a/apps/website/docs/guide/14-useful-utilities/index.mdx b/apps/website/docs/guide/14-useful-utilities/index.mdx index 16851f56..2bd7bc6f 100644 --- a/apps/website/docs/guide/14-useful-utilities/index.mdx +++ b/apps/website/docs/guide/14-useful-utilities/index.mdx @@ -97,27 +97,32 @@ const result = await queue.add(async () => await task()); ### Using External Storage -By default, these utilities store their data in memory. But if you're running multiple instances of your application (like on different servers), you'll want to use external storage like Redis so they can share information. +All utilities support custom storage implementations for distributed environments: ```typescript -// Example of how you might use Redis for rate limiting -class RedisRateLimitStorage implements RateLimitStorage { - async get(key: string): Promise { - // Get the current count from Redis - return (await redis.get(key)) || 0; - } - async set(key: string, value: number): Promise { - // Store the count in Redis - await redis.set(key, value); - } - async delete(key: string): Promise { - // Remove the count from Redis - await redis.del(key); - } -} +// Redis storage implementations are available from @commandkit/redis +import { + RedisRateLimitStorage, + RedisMutexStorage, + RedisSemaphoreStorage, +} from '@commandkit/redis'; +import { Redis } from 'ioredis'; + +const redis = new Redis(); + +// Rate limiting with Redis +const rateLimiter = createRateLimiter({ + storage: new RedisRateLimitStorage(redis), +}); + +// Mutex with Redis +const mutex = createMutex({ + storage: new RedisMutexStorage(redis), +}); -const limiter = createRateLimiter({ - storage: new RedisRateLimitStorage(), +// Semaphore with Redis +const semaphore = createSemaphore({ + storage: new RedisSemaphoreStorage(redis), }); ``` diff --git a/packages/redis/README.md b/packages/redis/README.md index f261d25f..b34686df 100644 --- a/packages/redis/README.md +++ b/packages/redis/README.md @@ -47,4 +47,127 @@ const redis = new Redis(); const redisProvider = new RedisCacheProvider(redis); setCacheProvider(redisProvider) +``` + +## Redis Mutex Storage + +This package also provides a Redis-based mutex storage implementation for distributed locking: + +```ts +import { createMutex } from 'commandkit/mutex'; +import { RedisMutexStorage } from '@commandkit/redis'; +import { Redis } from 'ioredis'; + +// Create Redis client +const redis = new Redis(); + +// Create Redis-based mutex storage +const redisMutexStorage = new RedisMutexStorage(redis); + +// Create mutex with Redis storage +const mutex = createMutex({ + timeout: 30000, + storage: redisMutexStorage, +}); + +// Use the mutex for distributed locking +const result = await mutex.withLock('shared-resource', async () => { + // This code runs with exclusive access across all instances + return await updateSharedResource(); +}); +``` + +### Redis Mutex Features + +- **Distributed Locking**: Works across multiple application instances +- **Automatic Expiration**: Locks automatically expire to prevent deadlocks +- **Abort Signal Support**: Can be cancelled using AbortSignal +- **Atomic Operations**: Uses Lua scripts for atomic lock operations +- **Lock Extension**: Extend lock timeouts if needed +- **Force Release**: Emergency release of locks (use with caution) + +### Advanced Mutex Usage + +```ts +import { RedisMutexStorage } from '@commandkit/redis'; + +const redisStorage = new RedisMutexStorage(redis); + +// Get detailed lock information +const lockInfo = await redisStorage.getLockInfo('my-resource'); +console.log(`Locked: ${lockInfo.locked}, TTL: ${lockInfo.ttl}ms`); + +// Extend a lock +const extended = await redisStorage.extendLock('my-resource', 60000); +if (extended) { + console.log('Lock extended by 60 seconds'); +} + +// Force release a lock (emergency use only) +await redisStorage.forceRelease('my-resource'); +``` + +## Redis Semaphore Storage + +This package also provides a Redis-based semaphore storage implementation for distributed concurrency control: + +```ts +import { createSemaphore } from 'commandkit/semaphore'; +import { RedisSemaphoreStorage } from '@commandkit/redis'; +import { Redis } from 'ioredis'; + +// Create Redis client +const redis = new Redis(); + +// Create Redis-based semaphore storage +const redisSemaphoreStorage = new RedisSemaphoreStorage(redis); + +// Create semaphore with Redis storage +const semaphore = createSemaphore({ + permits: 5, + timeout: 30000, + storage: redisSemaphoreStorage, +}); + +// Use the semaphore for distributed concurrency control +const result = await semaphore.withPermit('database-connection', async () => { + // This code runs with limited concurrency across all instances + return await executeDatabaseQuery(); +}); +``` + +### Redis Semaphore Features + +- **Distributed Concurrency Control**: Works across multiple application instances +- **Automatic Initialization**: Semaphores are automatically initialized when first used +- **Abort Signal Support**: Can be cancelled using AbortSignal +- **Atomic Operations**: Uses Lua scripts for atomic permit operations +- **Dynamic Permit Management**: Increase or decrease permits at runtime +- **Semaphore Information**: Get detailed information about permit usage + +### Advanced Semaphore Usage + +```ts +import { RedisSemaphoreStorage } from '@commandkit/redis'; + +const redisStorage = new RedisSemaphoreStorage(redis); + +// Get detailed semaphore information +const info = await redisStorage.getSemaphoreInfo('database'); +console.log(`Total: ${info.total}, Available: ${info.available}, Acquired: ${info.acquired}`); + +// Initialize a semaphore with specific permits +await redisStorage.initialize('api-endpoint', 10); + +// Increase permits dynamically +await redisStorage.increasePermits('database', 5); + +// Decrease permits dynamically +await redisStorage.decreasePermits('api-endpoint', 2); + +// Reset semaphore to initial state +await redisStorage.reset('database'); + +// Clear semaphore completely +await redisStorage.clear('old-semaphore'); ``` \ No newline at end of file diff --git a/packages/redis/src/index.ts b/packages/redis/src/index.ts index 5f55a342..ad405e66 100644 --- a/packages/redis/src/index.ts +++ b/packages/redis/src/index.ts @@ -150,3 +150,7 @@ export class RedisPlugin extends RuntimePlugin { export function redis(options?: RedisOptions) { return new RedisPlugin(options ?? {}); } + +export * from './ratelimit-storage'; +export * from './mutex-storage'; +export * from './semaphore-storage'; diff --git a/packages/redis/src/mutex-storage.ts b/packages/redis/src/mutex-storage.ts new file mode 100644 index 00000000..49ad8d93 --- /dev/null +++ b/packages/redis/src/mutex-storage.ts @@ -0,0 +1,126 @@ +import type { MutexStorage } from 'commandkit/mutex'; +import Redis from 'ioredis'; + +export class RedisMutexStorage implements MutexStorage { + private readonly lockPrefix = 'mutex:'; + private readonly defaultTimeout = 30000; // 30 seconds + + public constructor(private readonly redis: Redis) {} + + public async acquire( + key: string, + timeout: number = this.defaultTimeout, + signal?: AbortSignal, + ): Promise { + const lockKey = this.lockPrefix + key; + const lockValue = this.generateLockValue(); + const startTime = Date.now(); + + while (Date.now() - startTime < timeout) { + // Check if aborted + if (signal?.aborted) { + throw new Error('Lock acquisition was aborted'); + } + + // Try to acquire the lock using SET with NX (only if not exists) and EX (expiration) + const result = await this.redis.set( + lockKey, + lockValue, + 'EX', + Math.ceil(timeout / 1000), // Convert to seconds + 'NX', + ); + + if (result === 'OK') { + return true; + } + + // Wait a bit before trying again + await this.delay(10); + } + + return false; + } + + public async release(key: string): Promise { + const lockKey = this.lockPrefix + key; + + // Simple delete - in a real scenario, you might want to check ownership + // but for simplicity, we'll just delete the key + await this.redis.del(lockKey); + } + + public async isLocked(key: string): Promise { + const lockKey = this.lockPrefix + key; + const exists = await this.redis.exists(lockKey); + return exists === 1; + } + + /** + * Gets information about a lock including its TTL + * @param key - The lock key + * @returns Object containing lock information + */ + public async getLockInfo(key: string): Promise<{ + locked: boolean; + ttl: number; + value?: string; + }> { + const lockKey = this.lockPrefix + key; + const exists = await this.redis.exists(lockKey); + + if (exists === 0) { + return { locked: false, ttl: 0 }; + } + + const ttl = await this.redis.ttl(lockKey); + const value = await this.redis.get(lockKey); + + return { + locked: true, + ttl: ttl > 0 ? ttl * 1000 : 0, // Convert to milliseconds + value: value || undefined, + }; + } + + /** + * Force releases a lock (use with caution) + * @param key - The lock key + */ + public async forceRelease(key: string): Promise { + const lockKey = this.lockPrefix + key; + await this.redis.del(lockKey); + } + + /** + * Extends the lock timeout + * @param key - The lock key + * @param additionalTime - Additional time in milliseconds + * @returns True if lock was extended, false if lock doesn't exist + */ + public async extendLock( + key: string, + additionalTime: number, + ): Promise { + const lockKey = this.lockPrefix + key; + const exists = await this.redis.exists(lockKey); + + if (exists === 0) { + return false; + } + + const result = await this.redis.expire( + lockKey, + Math.ceil(additionalTime / 1000), + ); + return result === 1; + } + + private generateLockValue(): string { + return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + } + + private delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } +} diff --git a/packages/redis/src/ratelimit-storage.ts b/packages/redis/src/ratelimit-storage.ts new file mode 100644 index 00000000..647b5a43 --- /dev/null +++ b/packages/redis/src/ratelimit-storage.ts @@ -0,0 +1,18 @@ +import type { RateLimitStorage } from 'commandkit/ratelimit'; +import Redis from 'ioredis'; + +export class RedisRateLimitStorage implements RateLimitStorage { + public constructor(private readonly redis: Redis) {} + + public async get(key: string): Promise { + return Number(await this.redis.get(key)) || 0; + } + + public async set(key: string, value: number): Promise { + await this.redis.set(key, value); + } + + public async delete(key: string): Promise { + await this.redis.del(key); + } +} diff --git a/packages/redis/src/semaphore-storage.ts b/packages/redis/src/semaphore-storage.ts new file mode 100644 index 00000000..36885083 --- /dev/null +++ b/packages/redis/src/semaphore-storage.ts @@ -0,0 +1,196 @@ +import type { SemaphoreStorage } from 'commandkit/semaphore'; +import Redis from 'ioredis'; + +export class RedisSemaphoreStorage implements SemaphoreStorage { + private readonly semaphorePrefix = 'semaphore:'; + private readonly defaultTimeout = 30000; // 30 seconds + + public constructor(private readonly redis: Redis) {} + + public async acquire( + key: string, + timeout: number = this.defaultTimeout, + signal?: AbortSignal, + ): Promise { + const semaphoreKey = this.semaphorePrefix + key; + const startTime = Date.now(); + + while (Date.now() - startTime < timeout) { + // Check if aborted + if (signal?.aborted) { + throw new Error('Permit acquisition was aborted'); + } + + // Check if semaphore exists, if not initialize it + const total = await this.redis.get(semaphoreKey + ':total'); + if (!total) { + await this.initialize(key, 10); // Default 10 permits + } + + // Try to decrement available permits + const available = await this.redis.get(semaphoreKey + ':available'); + const availableCount = Number(available) || 0; + + if (availableCount > 0) { + // Use DECR to atomically decrease the count + const newCount = await this.redis.decr(semaphoreKey + ':available'); + if (newCount >= 0) { + return true; + } else { + // If we went negative, increment back + await this.redis.incr(semaphoreKey + ':available'); + } + } + + // Wait a bit before trying again + await this.delay(10); + } + + return false; + } + + public async release(key: string): Promise { + const semaphoreKey = this.semaphorePrefix + key; + + // Get current values + const total = await this.redis.get(semaphoreKey + ':total'); + const available = await this.redis.get(semaphoreKey + ':available'); + + const totalPermits = Number(total) || 0; + const availablePermits = Number(available) || 0; + + // Only increment if we haven't reached the total + if (availablePermits < totalPermits) { + await this.redis.incr(semaphoreKey + ':available'); + } + } + + public async getAvailablePermits(key: string): Promise { + const semaphoreKey = this.semaphorePrefix + key; + const available = await this.redis.get(semaphoreKey + ':available'); + return Number(available) || 0; + } + + public async getTotalPermits(key: string): Promise { + const semaphoreKey = this.semaphorePrefix + key; + const total = await this.redis.get(semaphoreKey + ':total'); + return Number(total) || 0; + } + + /** + * Initializes a semaphore with the specified number of permits + * @param key - The semaphore key + * @param permits - The total number of permits to allocate + */ + public async initialize(key: string, permits: number): Promise { + const semaphoreKey = this.semaphorePrefix + key; + + await this.redis.set(semaphoreKey + ':total', permits); + await this.redis.set(semaphoreKey + ':available', permits); + } + + /** + * Gets detailed information about a semaphore + * @param key - The semaphore key + * @returns Object containing semaphore information + */ + public async getSemaphoreInfo(key: string): Promise<{ + total: number; + available: number; + acquired: number; + initialized: boolean; + }> { + const semaphoreKey = this.semaphorePrefix + key; + const total = await this.redis.get(semaphoreKey + ':total'); + const available = await this.redis.get(semaphoreKey + ':available'); + + const totalPermits = Number(total) || 0; + const availablePermits = Number(available) || 0; + + return { + total: totalPermits, + available: availablePermits, + acquired: totalPermits - availablePermits, + initialized: totalPermits > 0, + }; + } + + /** + * Resets a semaphore to its initial state + * @param key - The semaphore key + * @param permits - The number of permits to reset to (optional, uses current total if not provided) + */ + public async reset(key: string, permits?: number): Promise { + const semaphoreKey = this.semaphorePrefix + key; + + if (permits !== undefined) { + await this.initialize(key, permits); + } else { + const total = await this.getTotalPermits(key); + if (total > 0) { + await this.redis.set(semaphoreKey + ':available', total); + } + } + } + + /** + * Increases the total number of permits for a semaphore + * @param key - The semaphore key + * @param additionalPermits - The number of additional permits to add + */ + public async increasePermits( + key: string, + additionalPermits: number, + ): Promise { + const semaphoreKey = this.semaphorePrefix + key; + + const total = await this.redis.get(semaphoreKey + ':total'); + const available = await this.redis.get(semaphoreKey + ':available'); + + const totalPermits = Number(total) || 0; + const availablePermits = Number(available) || 0; + + const newTotal = totalPermits + additionalPermits; + const newAvailable = availablePermits + additionalPermits; + + await this.redis.set(semaphoreKey + ':total', newTotal); + await this.redis.set(semaphoreKey + ':available', newAvailable); + } + + /** + * Decreases the total number of permits for a semaphore + * @param key - The semaphore key + * @param permitsToRemove - The number of permits to remove + */ + public async decreasePermits( + key: string, + permitsToRemove: number, + ): Promise { + const semaphoreKey = this.semaphorePrefix + key; + + const total = await this.redis.get(semaphoreKey + ':total'); + const available = await this.redis.get(semaphoreKey + ':available'); + + const totalPermits = Number(total) || 0; + const availablePermits = Number(available) || 0; + + const newTotal = Math.max(0, totalPermits - permitsToRemove); + const newAvailable = Math.max(0, availablePermits - permitsToRemove); + + await this.redis.set(semaphoreKey + ':total', newTotal); + await this.redis.set(semaphoreKey + ':available', newAvailable); + } + + /** + * Clears a semaphore completely + * @param key - The semaphore key + */ + public async clear(key: string): Promise { + const semaphoreKey = this.semaphorePrefix + key; + await this.redis.del(semaphoreKey + ':total', semaphoreKey + ':available'); + } + + private delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } +} From 75adfe9968b6df076bc1b8ec26df1333f27fe52b Mon Sep 17 00:00:00 2001 From: twlite <46562212+twlite@users.noreply.github.com> Date: Sun, 22 Jun 2025 19:43:58 +0545 Subject: [PATCH 3/3] docs: generate api docs --- .../classes/app-command-handler.mdx | 2 +- .../commandkit/classes/async-queue.mdx | 89 +++++++++++++++ .../classes/compiler-plugin-runtime.mdx | 4 +- .../classes/memory-mutex-storage.mdx | 51 +++++++++ .../classes/memory-rate-limit-storage.mdx | 51 +++++++++ .../classes/memory-semaphore-storage.mdx | 63 +++++++++++ .../commandkit/classes/mutex.mdx | 78 +++++++++++++ .../commandkit/classes/rate-limiter.mdx | 78 +++++++++++++ .../commandkit/classes/semaphore.mdx | 90 +++++++++++++++ .../commandkit/functions/acquire-lock.mdx | 36 ++++++ .../commandkit/functions/acquire-permit.mdx | 36 ++++++ .../functions/create-async-queue.mdx | 42 +++++++ .../commandkit/functions/create-mutex.mdx | 39 +++++++ .../functions/create-rate-limiter.mdx | 40 +++++++ .../commandkit/functions/create-semaphore.mdx | 40 +++++++ .../functions/get-acquired-permits.mdx | 28 +++++ .../functions/get-available-permits.mdx | 28 +++++ .../functions/get-remaining-requests.mdx | 28 +++++ .../commandkit/functions/get-reset-time.mdx | 28 +++++ .../commandkit/functions/is-locked.mdx | 28 +++++ .../commandkit/functions/on-bootstrap.mdx | 2 +- .../commandkit/functions/ratelimit.mdx | 48 ++++++++ .../commandkit/functions/release-lock.mdx | 28 +++++ .../commandkit/functions/release-permit.mdx | 28 +++++ .../commandkit/functions/reset-rate-limit.mdx | 28 +++++ .../commandkit/functions/with-mutex.mdx | 53 +++++++++ .../commandkit/functions/with-permit.mdx | 53 +++++++++ .../interfaces/app-command-native.mdx | 2 +- .../interfaces/async-queue-options.mdx | 48 ++++++++ .../interfaces/command-kit-config.mdx | 6 + .../interfaces/custom-app-command-props.mdx | 2 +- .../commandkit/interfaces/loaded-command.mdx | 2 +- .../commandkit/interfaces/mutex-options.mdx | 41 +++++++ .../commandkit/interfaces/mutex-storage.mdx | 52 +++++++++ .../prepared-app-command-execution.mdx | 2 +- .../interfaces/rate-limit-options.mdx | 47 ++++++++ .../interfaces/rate-limit-storage.mdx | 48 ++++++++ .../interfaces/semaphore-options.mdx | 47 ++++++++ .../interfaces/semaphore-storage.mdx | 58 ++++++++++ .../commandkit/types/app-command.mdx | 2 +- .../commandkit/types/async-queue-task.mdx | 22 ++++ .../commandkit/types/command-builder-like.mdx | 2 +- .../types/command-data-schema-key.mdx | 2 +- .../types/command-data-schema-value.mdx | 2 +- .../commandkit/types/command-data-schema.mdx | 2 +- .../commandkit/types/command-type-data.mdx | 2 +- .../commandkit/types/resolvable-command.mdx | 2 +- .../commandkit/types/run-command.mdx | 2 +- .../commandkit/variables/default-mutex.mdx | 19 ++++ .../variables/default-rate-limiter.mdx | 19 ++++ .../variables/default-semaphore.mdx | 19 ++++ .../variables/default_max_requests.mdx | 19 ++++ .../commandkit/variables/default_timeout.mdx | 19 ++++ .../redis/classes/redis-mutex-storage.mdx | 78 +++++++++++++ .../classes/redis-rate-limit-storage.mdx | 56 ++++++++++ .../redis/classes/redis-semaphore-storage.mdx | 103 ++++++++++++++++++ package.json | 2 +- packages/ai/package.json | 2 +- packages/analytics/package.json | 2 +- packages/cache/package.json | 2 +- packages/commandkit/async-queue.cjs | 10 +- packages/commandkit/env.cjs | 16 +-- packages/commandkit/mutex.cjs | 34 +++--- packages/commandkit/package.json | 2 +- packages/commandkit/ratelimit.cjs | 44 ++++---- packages/commandkit/semaphore.cjs | 38 +++---- packages/create-commandkit/package.json | 2 +- packages/devtools/package.json | 2 +- packages/i18n/package.json | 2 +- packages/legacy/package.json | 2 +- 70 files changed, 1908 insertions(+), 96 deletions(-) create mode 100644 apps/website/docs/api-reference/commandkit/classes/async-queue.mdx create mode 100644 apps/website/docs/api-reference/commandkit/classes/memory-mutex-storage.mdx create mode 100644 apps/website/docs/api-reference/commandkit/classes/memory-rate-limit-storage.mdx create mode 100644 apps/website/docs/api-reference/commandkit/classes/memory-semaphore-storage.mdx create mode 100644 apps/website/docs/api-reference/commandkit/classes/mutex.mdx create mode 100644 apps/website/docs/api-reference/commandkit/classes/rate-limiter.mdx create mode 100644 apps/website/docs/api-reference/commandkit/classes/semaphore.mdx create mode 100644 apps/website/docs/api-reference/commandkit/functions/acquire-lock.mdx create mode 100644 apps/website/docs/api-reference/commandkit/functions/acquire-permit.mdx create mode 100644 apps/website/docs/api-reference/commandkit/functions/create-async-queue.mdx create mode 100644 apps/website/docs/api-reference/commandkit/functions/create-mutex.mdx create mode 100644 apps/website/docs/api-reference/commandkit/functions/create-rate-limiter.mdx create mode 100644 apps/website/docs/api-reference/commandkit/functions/create-semaphore.mdx create mode 100644 apps/website/docs/api-reference/commandkit/functions/get-acquired-permits.mdx create mode 100644 apps/website/docs/api-reference/commandkit/functions/get-available-permits.mdx create mode 100644 apps/website/docs/api-reference/commandkit/functions/get-remaining-requests.mdx create mode 100644 apps/website/docs/api-reference/commandkit/functions/get-reset-time.mdx create mode 100644 apps/website/docs/api-reference/commandkit/functions/is-locked.mdx create mode 100644 apps/website/docs/api-reference/commandkit/functions/ratelimit.mdx create mode 100644 apps/website/docs/api-reference/commandkit/functions/release-lock.mdx create mode 100644 apps/website/docs/api-reference/commandkit/functions/release-permit.mdx create mode 100644 apps/website/docs/api-reference/commandkit/functions/reset-rate-limit.mdx create mode 100644 apps/website/docs/api-reference/commandkit/functions/with-mutex.mdx create mode 100644 apps/website/docs/api-reference/commandkit/functions/with-permit.mdx create mode 100644 apps/website/docs/api-reference/commandkit/interfaces/async-queue-options.mdx create mode 100644 apps/website/docs/api-reference/commandkit/interfaces/mutex-options.mdx create mode 100644 apps/website/docs/api-reference/commandkit/interfaces/mutex-storage.mdx create mode 100644 apps/website/docs/api-reference/commandkit/interfaces/rate-limit-options.mdx create mode 100644 apps/website/docs/api-reference/commandkit/interfaces/rate-limit-storage.mdx create mode 100644 apps/website/docs/api-reference/commandkit/interfaces/semaphore-options.mdx create mode 100644 apps/website/docs/api-reference/commandkit/interfaces/semaphore-storage.mdx create mode 100644 apps/website/docs/api-reference/commandkit/types/async-queue-task.mdx create mode 100644 apps/website/docs/api-reference/commandkit/variables/default-mutex.mdx create mode 100644 apps/website/docs/api-reference/commandkit/variables/default-rate-limiter.mdx create mode 100644 apps/website/docs/api-reference/commandkit/variables/default-semaphore.mdx create mode 100644 apps/website/docs/api-reference/commandkit/variables/default_max_requests.mdx create mode 100644 apps/website/docs/api-reference/commandkit/variables/default_timeout.mdx create mode 100644 apps/website/docs/api-reference/redis/classes/redis-mutex-storage.mdx create mode 100644 apps/website/docs/api-reference/redis/classes/redis-rate-limit-storage.mdx create mode 100644 apps/website/docs/api-reference/redis/classes/redis-semaphore-storage.mdx diff --git a/apps/website/docs/api-reference/commandkit/classes/app-command-handler.mdx b/apps/website/docs/api-reference/commandkit/classes/app-command-handler.mdx index edcffda6..1d67f03e 100644 --- a/apps/website/docs/api-reference/commandkit/classes/app-command-handler.mdx +++ b/apps/website/docs/api-reference/commandkit/classes/app-command-handler.mdx @@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## AppCommandHandler - + Handles application commands for CommandKit, including loading, registration, and execution. Manages both slash commands and message commands with middleware support. diff --git a/apps/website/docs/api-reference/commandkit/classes/async-queue.mdx b/apps/website/docs/api-reference/commandkit/classes/async-queue.mdx new file mode 100644 index 00000000..4c76d0be --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/classes/async-queue.mdx @@ -0,0 +1,89 @@ +--- +title: "AsyncQueue" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## AsyncQueue + + + + + +```ts title="Signature" +class AsyncQueue { + constructor(options: AsyncQueueOptions = {}) + add(task: AsyncQueueTask, signal?: AbortSignal) => Promise; + pause() => ; + resume() => ; + abort() => ; + clear() => ; + getRunning() => number; + getPending() => number; + isPaused() => boolean; + isAborted() => boolean; +} +``` + +
+ +### constructor + +AsyncQueueOptions = {}) => AsyncQueue`} /> + + +### add + +AsyncQueueTask<T>, signal?: AbortSignal) => Promise<T>`} /> + +Adds a task to the queue. +### pause + + `} /> + +Pauses the queue. No new tasks will be started until resumed. +### resume + + `} /> + +Resumes the queue and processes pending tasks. +### abort + + `} /> + +Aborts the queue, rejecting all pending tasks. +### clear + + `} /> + +Clears all pending tasks from the queue. +### getRunning + + number`} /> + +Returns the number of running tasks. +### getPending + + number`} /> + +Returns the number of pending tasks in the queue. +### isPaused + + boolean`} /> + +Returns true if the queue is currently paused. +### isAborted + + boolean`} /> + +Returns true if the queue has been aborted. + + +
diff --git a/apps/website/docs/api-reference/commandkit/classes/compiler-plugin-runtime.mdx b/apps/website/docs/api-reference/commandkit/classes/compiler-plugin-runtime.mdx index e797c621..9df5184e 100644 --- a/apps/website/docs/api-reference/commandkit/classes/compiler-plugin-runtime.mdx +++ b/apps/website/docs/api-reference/commandkit/classes/compiler-plugin-runtime.mdx @@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## CompilerPluginRuntime - + CompilerPluginRuntime is a runtime for managing compiler plugins in CommandKit. @@ -66,7 +66,7 @@ This method must be called inside the activate() method of a plugin. `} /> -Unregisters a template handler for a given name. +Unregister a template handler for a given name. This method must be called inside the deactivate() method of a plugin. ### getTemplate diff --git a/apps/website/docs/api-reference/commandkit/classes/memory-mutex-storage.mdx b/apps/website/docs/api-reference/commandkit/classes/memory-mutex-storage.mdx new file mode 100644 index 00000000..b8dcac73 --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/classes/memory-mutex-storage.mdx @@ -0,0 +1,51 @@ +--- +title: "MemoryMutexStorage" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## MemoryMutexStorage + + + +In-memory storage implementation for mutex locks. +Suitable for single-instance applications. + +```ts title="Signature" +class MemoryMutexStorage implements MutexStorage { + acquire(key: string, timeout: number = 30000, signal?: AbortSignal) => Promise; + release(key: string) => Promise; + isLocked(key: string) => Promise; +} +``` +* Implements: MutexStorage + + + +
+ +### acquire + + Promise<boolean>`} /> + +Attempts to acquire a lock for a given key +### release + + Promise<void>`} /> + +Releases the lock for a given key +### isLocked + + Promise<boolean>`} /> + +Checks if a lock is currently held for a given key + + +
diff --git a/apps/website/docs/api-reference/commandkit/classes/memory-rate-limit-storage.mdx b/apps/website/docs/api-reference/commandkit/classes/memory-rate-limit-storage.mdx new file mode 100644 index 00000000..195603c1 --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/classes/memory-rate-limit-storage.mdx @@ -0,0 +1,51 @@ +--- +title: "MemoryRateLimitStorage" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## MemoryRateLimitStorage + + + +In-memory storage implementation for rate limiting. +Suitable for single-instance applications. + +```ts title="Signature" +class MemoryRateLimitStorage implements RateLimitStorage { + get(key: string) => Promise; + set(key: string, value: number) => Promise; + delete(key: string) => Promise; +} +``` +* Implements: RateLimitStorage + + + +
+ +### get + + Promise<number>`} /> + +Retrieves the current request count for a given key +### set + + Promise<void>`} /> + +Sets the request count for a given key +### delete + + Promise<void>`} /> + +Deletes the rate limit entry for a given key + + +
diff --git a/apps/website/docs/api-reference/commandkit/classes/memory-semaphore-storage.mdx b/apps/website/docs/api-reference/commandkit/classes/memory-semaphore-storage.mdx new file mode 100644 index 00000000..c3780dc0 --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/classes/memory-semaphore-storage.mdx @@ -0,0 +1,63 @@ +--- +title: "MemorySemaphoreStorage" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## MemorySemaphoreStorage + + + +In-memory storage implementation for semaphore permits. +Suitable for single-instance applications. + +```ts title="Signature" +class MemorySemaphoreStorage implements SemaphoreStorage { + acquire(key: string, timeout: number = 30000, signal?: AbortSignal) => Promise; + release(key: string) => Promise; + getAvailablePermits(key: string) => Promise; + getTotalPermits(key: string) => Promise; + initialize(key: string, permits: number) => void; +} +``` +* Implements: SemaphoreStorage + + + +
+ +### acquire + + Promise<boolean>`} /> + +Attempts to acquire a permit for a given key +### release + + Promise<void>`} /> + +Releases a permit for a given key +### getAvailablePermits + + Promise<number>`} /> + +Gets the number of available permits for a given key +### getTotalPermits + + Promise<number>`} /> + +Gets the total number of permits for a given key +### initialize + + void`} /> + +Initializes a semaphore with the specified number of permits + + +
diff --git a/apps/website/docs/api-reference/commandkit/classes/mutex.mdx b/apps/website/docs/api-reference/commandkit/classes/mutex.mdx new file mode 100644 index 00000000..8010bc1a --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/classes/mutex.mdx @@ -0,0 +1,78 @@ +--- +title: "Mutex" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## Mutex + + + +Async mutex implementation that provides mutual exclusion for shared resources. +Ensures only one task can access a protected resource at a time. + +```ts title="Signature" +class Mutex { + constructor(options: MutexOptions = {}) + setStorage(storage: MutexStorage) => ; + getStorage() => MutexStorage; + acquire(key: string, timeout?: number, signal?: AbortSignal) => Promise; + release(key: string) => Promise; + isLocked(key: string) => Promise; + withLock(key: string, fn: () => Promise | T, timeout?: number, signal?: AbortSignal) => Promise; + getConfig() => Omit; +} +``` + +
+ +### constructor + +MutexOptions = {}) => Mutex`} /> + +Creates a new mutex instance +### setStorage + +MutexStorage) => `} /> + +Sets the storage implementation for the mutex +### getStorage + + MutexStorage`} /> + +Gets the storage implementation for the mutex +### acquire + + Promise<boolean>`} /> + +Acquires a lock for the given key +### release + + Promise<void>`} /> + +Releases the lock for the given key +### isLocked + + Promise<boolean>`} /> + +Checks if a lock is currently held for the given key +### withLock + + Promise<T>`} /> + +Executes a function with exclusive access to the resource +### getConfig + + Omit<MutexOptions, 'storage'>`} /> + +Gets the current configuration of the mutex + + +
diff --git a/apps/website/docs/api-reference/commandkit/classes/rate-limiter.mdx b/apps/website/docs/api-reference/commandkit/classes/rate-limiter.mdx new file mode 100644 index 00000000..8899459e --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/classes/rate-limiter.mdx @@ -0,0 +1,78 @@ +--- +title: "RateLimiter" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## RateLimiter + + + +Rate limiter implementation that enforces request limits per key. +Supports configurable request limits and time intervals. + +```ts title="Signature" +class RateLimiter { + constructor(maxRequests: number = DEFAULT_MAX_REQUESTS, interval: number = DEFAULT_TIMEOUT, storage: RateLimitStorage = new MemoryRateLimitStorage()) + setStorage(storage: RateLimitStorage) => ; + getStorage() => RateLimitStorage; + limit(key: string) => Promise; + getRemaining(key: string) => Promise; + getResetTime(key: string) => Promise; + reset(key: string) => Promise; + getConfig() => Omit; +} +``` + +
+ +### constructor + +RateLimitStorage = new MemoryRateLimitStorage()) => RateLimiter`} /> + +Creates a new rate limiter instance +### setStorage + +RateLimitStorage) => `} /> + +Sets the storage implementation for the rate limiter +### getStorage + + RateLimitStorage`} /> + +Gets the storage implementation for the rate limiter +### limit + + Promise<boolean>`} /> + +Checks if a request is allowed for the given key and increments the counter if allowed +### getRemaining + + Promise<number>`} /> + +Gets the remaining requests allowed for a given key +### getResetTime + + Promise<number>`} /> + +Gets the time until the rate limit resets for a given key +### reset + + Promise<void>`} /> + +Resets the rate limit for a given key +### getConfig + + Omit<RateLimitOptions, 'storage'>`} /> + +Gets the current configuration of the rate limiter + + +
diff --git a/apps/website/docs/api-reference/commandkit/classes/semaphore.mdx b/apps/website/docs/api-reference/commandkit/classes/semaphore.mdx new file mode 100644 index 00000000..f6eba274 --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/classes/semaphore.mdx @@ -0,0 +1,90 @@ +--- +title: "Semaphore" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## Semaphore + + + +Async semaphore implementation that controls access to a limited pool of resources. +Allows a specified number of concurrent operations while blocking additional requests. + +```ts title="Signature" +class Semaphore { + constructor(options: SemaphoreOptions = {}) + setStorage(storage: SemaphoreStorage) => ; + getStorage() => SemaphoreStorage; + acquire(key: string, timeout?: number, signal?: AbortSignal) => Promise; + release(key: string) => Promise; + getAvailablePermits(key: string) => Promise; + getTotalPermits(key: string) => Promise; + getAcquiredPermits(key: string) => Promise; + withPermit(key: string, fn: () => Promise | T, timeout?: number, signal?: AbortSignal) => Promise; + getConfig() => Omit; +} +``` + +
+ +### constructor + +SemaphoreOptions = {}) => Semaphore`} /> + +Creates a new semaphore instance +### setStorage + +SemaphoreStorage) => `} /> + +Sets the storage implementation for the semaphore +### getStorage + + SemaphoreStorage`} /> + +Gets the storage implementation for the semaphore +### acquire + + Promise<boolean>`} /> + +Acquires a permit for the given key +### release + + Promise<void>`} /> + +Releases a permit for the given key +### getAvailablePermits + + Promise<number>`} /> + +Gets the number of available permits for the given key +### getTotalPermits + + Promise<number>`} /> + +Gets the total number of permits for the given key +### getAcquiredPermits + + Promise<number>`} /> + +Gets the number of acquired permits for the given key +### withPermit + + Promise<T>`} /> + +Executes a function with a permit from the semaphore +### getConfig + + Omit<SemaphoreOptions, 'storage'>`} /> + +Gets the current configuration of the semaphore + + +
diff --git a/apps/website/docs/api-reference/commandkit/functions/acquire-lock.mdx b/apps/website/docs/api-reference/commandkit/functions/acquire-lock.mdx new file mode 100644 index 00000000..0191db5a --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/functions/acquire-lock.mdx @@ -0,0 +1,36 @@ +--- +title: "AcquireLock" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## acquireLock + + + +Acquires a lock using the default mutex + +```ts title="Signature" +function acquireLock(key: string, timeout?: number, signal?: AbortSignal): Promise +``` +Parameters + +### key + + + +### timeout + + + +### signal + + + diff --git a/apps/website/docs/api-reference/commandkit/functions/acquire-permit.mdx b/apps/website/docs/api-reference/commandkit/functions/acquire-permit.mdx new file mode 100644 index 00000000..f4928800 --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/functions/acquire-permit.mdx @@ -0,0 +1,36 @@ +--- +title: "AcquirePermit" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## acquirePermit + + + +Acquires a permit using the default semaphore + +```ts title="Signature" +function acquirePermit(key: string, timeout?: number, signal?: AbortSignal): Promise +``` +Parameters + +### key + + + +### timeout + + + +### signal + + + diff --git a/apps/website/docs/api-reference/commandkit/functions/create-async-queue.mdx b/apps/website/docs/api-reference/commandkit/functions/create-async-queue.mdx new file mode 100644 index 00000000..648c96e4 --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/functions/create-async-queue.mdx @@ -0,0 +1,42 @@ +--- +title: "CreateAsyncQueue" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## createAsyncQueue + + + +Creates a new async queue instance with the specified configuration. + + + +*Example* + +```typescript +const controller = new AbortController(); +const queue = createAsyncQueue({ + concurrency: 2, + signal: controller.signal +}); +queue.add(async () => await doSomething()); +controller.abort(); // Aborts the queue +``` + +```ts title="Signature" +function createAsyncQueue(options?: AsyncQueueOptions): AsyncQueue +``` +Parameters + +### options + +AsyncQueueOptions`} /> + diff --git a/apps/website/docs/api-reference/commandkit/functions/create-mutex.mdx b/apps/website/docs/api-reference/commandkit/functions/create-mutex.mdx new file mode 100644 index 00000000..4db4b649 --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/functions/create-mutex.mdx @@ -0,0 +1,39 @@ +--- +title: "CreateMutex" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## createMutex + + + +Creates a new mutex instance with the specified configuration + + + +*Example* + +```typescript +const mutex = createMutex({ + timeout: 60000, + storage: new RedisMutexStorage() +}); +``` + +```ts title="Signature" +function createMutex(options: MutexOptions): Mutex +``` +Parameters + +### options + +MutexOptions`} /> + diff --git a/apps/website/docs/api-reference/commandkit/functions/create-rate-limiter.mdx b/apps/website/docs/api-reference/commandkit/functions/create-rate-limiter.mdx new file mode 100644 index 00000000..16f54d88 --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/functions/create-rate-limiter.mdx @@ -0,0 +1,40 @@ +--- +title: "CreateRateLimiter" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## createRateLimiter + + + +Creates a new rate limiter instance with the specified configuration + + + +*Example* + +```typescript +const limiter = createRateLimiter({ + maxRequests: 5, + interval: 30000, + storage: new CustomStorage() +}); +``` + +```ts title="Signature" +function createRateLimiter(options: RateLimitOptions): RateLimiter +``` +Parameters + +### options + +RateLimitOptions`} /> + diff --git a/apps/website/docs/api-reference/commandkit/functions/create-semaphore.mdx b/apps/website/docs/api-reference/commandkit/functions/create-semaphore.mdx new file mode 100644 index 00000000..b5b2c0c1 --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/functions/create-semaphore.mdx @@ -0,0 +1,40 @@ +--- +title: "CreateSemaphore" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## createSemaphore + + + +Creates a new semaphore instance with the specified configuration + + + +*Example* + +```typescript +const semaphore = createSemaphore({ + permits: 5, + timeout: 60000, + storage: new RedisSemaphoreStorage() +}); +``` + +```ts title="Signature" +function createSemaphore(options: SemaphoreOptions): Semaphore +``` +Parameters + +### options + +SemaphoreOptions`} /> + diff --git a/apps/website/docs/api-reference/commandkit/functions/get-acquired-permits.mdx b/apps/website/docs/api-reference/commandkit/functions/get-acquired-permits.mdx new file mode 100644 index 00000000..b81a019f --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/functions/get-acquired-permits.mdx @@ -0,0 +1,28 @@ +--- +title: "GetAcquiredPermits" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## getAcquiredPermits + + + +Gets the number of acquired permits using the default semaphore + +```ts title="Signature" +function getAcquiredPermits(key: string): Promise +``` +Parameters + +### key + + + diff --git a/apps/website/docs/api-reference/commandkit/functions/get-available-permits.mdx b/apps/website/docs/api-reference/commandkit/functions/get-available-permits.mdx new file mode 100644 index 00000000..40977bc1 --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/functions/get-available-permits.mdx @@ -0,0 +1,28 @@ +--- +title: "GetAvailablePermits" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## getAvailablePermits + + + +Gets the number of available permits using the default semaphore + +```ts title="Signature" +function getAvailablePermits(key: string): Promise +``` +Parameters + +### key + + + diff --git a/apps/website/docs/api-reference/commandkit/functions/get-remaining-requests.mdx b/apps/website/docs/api-reference/commandkit/functions/get-remaining-requests.mdx new file mode 100644 index 00000000..8fbb08f3 --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/functions/get-remaining-requests.mdx @@ -0,0 +1,28 @@ +--- +title: "GetRemainingRequests" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## getRemainingRequests + + + +Gets the remaining requests for a key using the default rate limiter + +```ts title="Signature" +function getRemainingRequests(key: string): Promise +``` +Parameters + +### key + + + diff --git a/apps/website/docs/api-reference/commandkit/functions/get-reset-time.mdx b/apps/website/docs/api-reference/commandkit/functions/get-reset-time.mdx new file mode 100644 index 00000000..cd2bb906 --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/functions/get-reset-time.mdx @@ -0,0 +1,28 @@ +--- +title: "GetResetTime" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## getResetTime + + + +Gets the reset time for a key using the default rate limiter + +```ts title="Signature" +function getResetTime(key: string): Promise +``` +Parameters + +### key + + + diff --git a/apps/website/docs/api-reference/commandkit/functions/is-locked.mdx b/apps/website/docs/api-reference/commandkit/functions/is-locked.mdx new file mode 100644 index 00000000..23452786 --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/functions/is-locked.mdx @@ -0,0 +1,28 @@ +--- +title: "IsLocked" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## isLocked + + + +Checks if a lock is held using the default mutex + +```ts title="Signature" +function isLocked(key: string): Promise +``` +Parameters + +### key + + + diff --git a/apps/website/docs/api-reference/commandkit/functions/on-bootstrap.mdx b/apps/website/docs/api-reference/commandkit/functions/on-bootstrap.mdx index b0aa4775..cdc0e9a8 100644 --- a/apps/website/docs/api-reference/commandkit/functions/on-bootstrap.mdx +++ b/apps/website/docs/api-reference/commandkit/functions/on-bootstrap.mdx @@ -17,7 +17,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; Registers a bootstrap hook that will be called when the CommandKit instance is created. This is useful for plugins that need to run some code after the CommandKit instance is fully initialized. -Note that not all commandkit dependiencs are available at this point. It is recommended to use the `onApplicationBootstrap` hook instead, +Note that not all commandkit dependencies are available at this point. It is recommended to use the `onApplicationBootstrap` hook instead, if you need access to the commandkit dependencies. diff --git a/apps/website/docs/api-reference/commandkit/functions/ratelimit.mdx b/apps/website/docs/api-reference/commandkit/functions/ratelimit.mdx new file mode 100644 index 00000000..778018f8 --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/functions/ratelimit.mdx @@ -0,0 +1,48 @@ +--- +title: "Ratelimit" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## ratelimit + + + +Convenience function to check if a request is allowed for a given key. +Uses the default rate limiter instance. + + + +*Example* + +```typescript +const allowed = await ratelimit('user:123'); +// update the default rate limiter config +import { defaultRateLimiter } from 'commandkit/ratelimit'; + +// update max allowed requests +defaultRateLimiter.setMaxRequests(10); + +// update the timeout interval +defaultRateLimiter.setInterval(30000); + +// update the storage implementation +defaultRateLimiter.setStorage(new RedisStorage()); +``` + +```ts title="Signature" +function ratelimit(key: string): Promise +``` +Parameters + +### key + + + diff --git a/apps/website/docs/api-reference/commandkit/functions/release-lock.mdx b/apps/website/docs/api-reference/commandkit/functions/release-lock.mdx new file mode 100644 index 00000000..ff7a3e13 --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/functions/release-lock.mdx @@ -0,0 +1,28 @@ +--- +title: "ReleaseLock" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## releaseLock + + + +Releases a lock using the default mutex + +```ts title="Signature" +function releaseLock(key: string): Promise +``` +Parameters + +### key + + + diff --git a/apps/website/docs/api-reference/commandkit/functions/release-permit.mdx b/apps/website/docs/api-reference/commandkit/functions/release-permit.mdx new file mode 100644 index 00000000..092ac2ef --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/functions/release-permit.mdx @@ -0,0 +1,28 @@ +--- +title: "ReleasePermit" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## releasePermit + + + +Releases a permit using the default semaphore + +```ts title="Signature" +function releasePermit(key: string): Promise +``` +Parameters + +### key + + + diff --git a/apps/website/docs/api-reference/commandkit/functions/reset-rate-limit.mdx b/apps/website/docs/api-reference/commandkit/functions/reset-rate-limit.mdx new file mode 100644 index 00000000..58d3a4e8 --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/functions/reset-rate-limit.mdx @@ -0,0 +1,28 @@ +--- +title: "ResetRateLimit" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## resetRateLimit + + + +Resets the rate limit for a key using the default rate limiter + +```ts title="Signature" +function resetRateLimit(key: string): Promise +``` +Parameters + +### key + + + diff --git a/apps/website/docs/api-reference/commandkit/functions/with-mutex.mdx b/apps/website/docs/api-reference/commandkit/functions/with-mutex.mdx new file mode 100644 index 00000000..94d5b5ae --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/functions/with-mutex.mdx @@ -0,0 +1,53 @@ +--- +title: "WithMutex" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## withMutex + + + +Convenience function to execute a function with exclusive access using the default mutex. + + + +*Example* + +```typescript +const controller = new AbortController(); +const result = await withMutex('shared-resource', async () => { + // This code runs with exclusive access + return await updateSharedResource(); +}, 30000, controller.signal); +controller.abort(); // Cancels the lock acquisition +``` + +```ts title="Signature" +function withMutex(key: string, fn: () => Promise | T, timeout?: number, signal?: AbortSignal): Promise +``` +Parameters + +### key + + + +### fn + + + +### timeout + + + +### signal + + + diff --git a/apps/website/docs/api-reference/commandkit/functions/with-permit.mdx b/apps/website/docs/api-reference/commandkit/functions/with-permit.mdx new file mode 100644 index 00000000..5f46ebc5 --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/functions/with-permit.mdx @@ -0,0 +1,53 @@ +--- +title: "WithPermit" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## withPermit + + + +Convenience function to execute a function with a permit using the default semaphore. + + + +*Example* + +```typescript +const controller = new AbortController(); +const result = await withPermit('database-connection', async () => { + // This code runs with a permit from the semaphore + return await executeDatabaseQuery(); +}, 30000, controller.signal); +controller.abort(); // Cancels the permit acquisition +``` + +```ts title="Signature" +function withPermit(key: string, fn: () => Promise | T, timeout?: number, signal?: AbortSignal): Promise +``` +Parameters + +### key + + + +### fn + + + +### timeout + + + +### signal + + + diff --git a/apps/website/docs/api-reference/commandkit/interfaces/app-command-native.mdx b/apps/website/docs/api-reference/commandkit/interfaces/app-command-native.mdx index 38745564..36106df2 100644 --- a/apps/website/docs/api-reference/commandkit/interfaces/app-command-native.mdx +++ b/apps/website/docs/api-reference/commandkit/interfaces/app-command-native.mdx @@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## AppCommandNative - + Represents a native command structure used in CommandKit. This structure includes the command definition and various handlers for different interaction types. diff --git a/apps/website/docs/api-reference/commandkit/interfaces/async-queue-options.mdx b/apps/website/docs/api-reference/commandkit/interfaces/async-queue-options.mdx new file mode 100644 index 00000000..66869289 --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/interfaces/async-queue-options.mdx @@ -0,0 +1,48 @@ +--- +title: "AsyncQueueOptions" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## AsyncQueueOptions + + + +Async queue implementation for processing tasks sequentially or with limited concurrency. +Useful for rate-limiting, batching, or controlling resource usage. + +```ts title="Signature" +interface AsyncQueueOptions { + concurrency?: number; + onDrain?: () => void | Promise; + signal?: AbortSignal; +} +``` + +
+ +### concurrency + + + +Maximum number of concurrent tasks. Default: 1 (sequential) +### onDrain + + + +Optional callback invoked when all tasks are completed +### signal + + + +Optional AbortSignal for cancelling the queue + + +
diff --git a/apps/website/docs/api-reference/commandkit/interfaces/command-kit-config.mdx b/apps/website/docs/api-reference/commandkit/interfaces/command-kit-config.mdx index bd0e081e..fbb793a3 100644 --- a/apps/website/docs/api-reference/commandkit/interfaces/command-kit-config.mdx +++ b/apps/website/docs/api-reference/commandkit/interfaces/command-kit-config.mdx @@ -53,6 +53,7 @@ interface CommandKitConfig { production?: boolean; }; typedCommands?: boolean; + disablePrefixCommands?: boolean; } ``` @@ -104,6 +105,11 @@ Whether or not to enable the source map generation. Whether or not to enable the typed commands. +### disablePrefixCommands + + + +Whether or not to disable the prefix commands. diff --git a/apps/website/docs/api-reference/commandkit/interfaces/custom-app-command-props.mdx b/apps/website/docs/api-reference/commandkit/interfaces/custom-app-command-props.mdx index 13705da7..b482ad50 100644 --- a/apps/website/docs/api-reference/commandkit/interfaces/custom-app-command-props.mdx +++ b/apps/website/docs/api-reference/commandkit/interfaces/custom-app-command-props.mdx @@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## CustomAppCommandProps - + Custom properties that can be added to an AppCommand. This allows for additional metadata or configuration to be associated with a command. diff --git a/apps/website/docs/api-reference/commandkit/interfaces/loaded-command.mdx b/apps/website/docs/api-reference/commandkit/interfaces/loaded-command.mdx index 998315f2..13d1c314 100644 --- a/apps/website/docs/api-reference/commandkit/interfaces/loaded-command.mdx +++ b/apps/website/docs/api-reference/commandkit/interfaces/loaded-command.mdx @@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## LoadedCommand - + Represents a loaded command with its metadata and configuration. diff --git a/apps/website/docs/api-reference/commandkit/interfaces/mutex-options.mdx b/apps/website/docs/api-reference/commandkit/interfaces/mutex-options.mdx new file mode 100644 index 00000000..59ef2503 --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/interfaces/mutex-options.mdx @@ -0,0 +1,41 @@ +--- +title: "MutexOptions" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## MutexOptions + + + +Configuration options for mutex + +```ts title="Signature" +interface MutexOptions { + timeout?: number; + storage?: MutexStorage; +} +``` + +
+ +### timeout + + + +Default timeout in milliseconds for lock acquisition. Default: 30000 +### storage + +MutexStorage`} /> + +Storage implementation for persisting mutex data. Default: MemoryMutexStorage + + +
diff --git a/apps/website/docs/api-reference/commandkit/interfaces/mutex-storage.mdx b/apps/website/docs/api-reference/commandkit/interfaces/mutex-storage.mdx new file mode 100644 index 00000000..ef89b7ab --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/interfaces/mutex-storage.mdx @@ -0,0 +1,52 @@ +--- +title: "MutexStorage" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## MutexStorage + + + +Interface for mutex storage implementations. +Provides methods to store, retrieve, and delete mutex lock data. + +```ts title="Signature" +interface MutexStorage { + acquire( + key: string, + timeout?: number, + signal?: AbortSignal, + ): Promise; + release(key: string): Promise; + isLocked(key: string): Promise; +} +``` + +
+ +### acquire + + Promise<boolean>`} /> + +Attempts to acquire a lock for a given key +### release + + Promise<void>`} /> + +Releases the lock for a given key +### isLocked + + Promise<boolean>`} /> + +Checks if a lock is currently held for a given key + + +
diff --git a/apps/website/docs/api-reference/commandkit/interfaces/prepared-app-command-execution.mdx b/apps/website/docs/api-reference/commandkit/interfaces/prepared-app-command-execution.mdx index 31c33be2..9ef995db 100644 --- a/apps/website/docs/api-reference/commandkit/interfaces/prepared-app-command-execution.mdx +++ b/apps/website/docs/api-reference/commandkit/interfaces/prepared-app-command-execution.mdx @@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## PreparedAppCommandExecution - + Represents a prepared command execution with all necessary data and middleware. diff --git a/apps/website/docs/api-reference/commandkit/interfaces/rate-limit-options.mdx b/apps/website/docs/api-reference/commandkit/interfaces/rate-limit-options.mdx new file mode 100644 index 00000000..5f64cbe9 --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/interfaces/rate-limit-options.mdx @@ -0,0 +1,47 @@ +--- +title: "RateLimitOptions" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## RateLimitOptions + + + +Configuration options for rate limiting + +```ts title="Signature" +interface RateLimitOptions { + maxRequests?: number; + interval?: number; + storage?: RateLimitStorage; +} +``` + +
+ +### maxRequests + + + +Maximum number of requests allowed per interval. Default: 10 +### interval + + + +Time interval in milliseconds for the rate limit window. Default: 60000 +### storage + +RateLimitStorage`} /> + +Storage implementation for persisting rate limit data. Default: MemoryRateLimitStorage + + +
diff --git a/apps/website/docs/api-reference/commandkit/interfaces/rate-limit-storage.mdx b/apps/website/docs/api-reference/commandkit/interfaces/rate-limit-storage.mdx new file mode 100644 index 00000000..d8c0c59b --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/interfaces/rate-limit-storage.mdx @@ -0,0 +1,48 @@ +--- +title: "RateLimitStorage" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## RateLimitStorage + + + +Interface for rate limit storage implementations. +Provides methods to store, retrieve, and delete rate limit data. + +```ts title="Signature" +interface RateLimitStorage { + get(key: string): Promise; + set(key: string, value: number): Promise; + delete(key: string): Promise; +} +``` + +
+ +### get + + Promise<number>`} /> + +Retrieves the current request count for a given key +### set + + Promise<void>`} /> + +Sets the request count for a given key +### delete + + Promise<void>`} /> + +Deletes the rate limit entry for a given key + + +
diff --git a/apps/website/docs/api-reference/commandkit/interfaces/semaphore-options.mdx b/apps/website/docs/api-reference/commandkit/interfaces/semaphore-options.mdx new file mode 100644 index 00000000..b90af75f --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/interfaces/semaphore-options.mdx @@ -0,0 +1,47 @@ +--- +title: "SemaphoreOptions" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## SemaphoreOptions + + + +Configuration options for semaphore + +```ts title="Signature" +interface SemaphoreOptions { + permits?: number; + timeout?: number; + storage?: SemaphoreStorage; +} +``` + +
+ +### permits + + + +Maximum number of concurrent permits allowed. Default: 1 +### timeout + + + +Default timeout in milliseconds for permit acquisition. Default: 30000 +### storage + +SemaphoreStorage`} /> + +Storage implementation for persisting semaphore data. Default: MemorySemaphoreStorage + + +
diff --git a/apps/website/docs/api-reference/commandkit/interfaces/semaphore-storage.mdx b/apps/website/docs/api-reference/commandkit/interfaces/semaphore-storage.mdx new file mode 100644 index 00000000..3d98d92e --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/interfaces/semaphore-storage.mdx @@ -0,0 +1,58 @@ +--- +title: "SemaphoreStorage" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## SemaphoreStorage + + + +Interface for semaphore storage implementations. +Provides methods to store, retrieve, and manage semaphore permit data. + +```ts title="Signature" +interface SemaphoreStorage { + acquire( + key: string, + timeout?: number, + signal?: AbortSignal, + ): Promise; + release(key: string): Promise; + getAvailablePermits(key: string): Promise; + getTotalPermits(key: string): Promise; +} +``` + +
+ +### acquire + + Promise<boolean>`} /> + +Attempts to acquire a permit for a given key +### release + + Promise<void>`} /> + +Releases a permit for a given key +### getAvailablePermits + + Promise<number>`} /> + +Gets the number of available permits for a given key +### getTotalPermits + + Promise<number>`} /> + +Gets the total number of permits for a given key + + +
diff --git a/apps/website/docs/api-reference/commandkit/types/app-command.mdx b/apps/website/docs/api-reference/commandkit/types/app-command.mdx index 6c08f161..7e1b4d52 100644 --- a/apps/website/docs/api-reference/commandkit/types/app-command.mdx +++ b/apps/website/docs/api-reference/commandkit/types/app-command.mdx @@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## AppCommand - + Represents a command in the CommandKit application, including its metadata and handlers. This type extends the native command structure with additional properties. diff --git a/apps/website/docs/api-reference/commandkit/types/async-queue-task.mdx b/apps/website/docs/api-reference/commandkit/types/async-queue-task.mdx new file mode 100644 index 00000000..d347a424 --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/types/async-queue-task.mdx @@ -0,0 +1,22 @@ +--- +title: "AsyncQueueTask" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## AsyncQueueTask + + + + + +```ts title="Signature" +type AsyncQueueTask = () => Promise +``` diff --git a/apps/website/docs/api-reference/commandkit/types/command-builder-like.mdx b/apps/website/docs/api-reference/commandkit/types/command-builder-like.mdx index 30c3ed1f..93c373fd 100644 --- a/apps/website/docs/api-reference/commandkit/types/command-builder-like.mdx +++ b/apps/website/docs/api-reference/commandkit/types/command-builder-like.mdx @@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## CommandBuilderLike - + Type representing command builder objects supported by CommandKit. diff --git a/apps/website/docs/api-reference/commandkit/types/command-data-schema-key.mdx b/apps/website/docs/api-reference/commandkit/types/command-data-schema-key.mdx index d5fe2e01..39f66319 100644 --- a/apps/website/docs/api-reference/commandkit/types/command-data-schema-key.mdx +++ b/apps/website/docs/api-reference/commandkit/types/command-data-schema-key.mdx @@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## CommandDataSchemaKey - + diff --git a/apps/website/docs/api-reference/commandkit/types/command-data-schema-value.mdx b/apps/website/docs/api-reference/commandkit/types/command-data-schema-value.mdx index 196a32fc..d0bc09c5 100644 --- a/apps/website/docs/api-reference/commandkit/types/command-data-schema-value.mdx +++ b/apps/website/docs/api-reference/commandkit/types/command-data-schema-value.mdx @@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## CommandDataSchemaValue - + diff --git a/apps/website/docs/api-reference/commandkit/types/command-data-schema.mdx b/apps/website/docs/api-reference/commandkit/types/command-data-schema.mdx index e4284f18..e14a0d40 100644 --- a/apps/website/docs/api-reference/commandkit/types/command-data-schema.mdx +++ b/apps/website/docs/api-reference/commandkit/types/command-data-schema.mdx @@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## CommandDataSchema - + diff --git a/apps/website/docs/api-reference/commandkit/types/command-type-data.mdx b/apps/website/docs/api-reference/commandkit/types/command-type-data.mdx index b2a06c35..7e6528b3 100644 --- a/apps/website/docs/api-reference/commandkit/types/command-type-data.mdx +++ b/apps/website/docs/api-reference/commandkit/types/command-type-data.mdx @@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## CommandTypeData - + Type representing command data identifier. diff --git a/apps/website/docs/api-reference/commandkit/types/resolvable-command.mdx b/apps/website/docs/api-reference/commandkit/types/resolvable-command.mdx index be379f6b..f7c894eb 100644 --- a/apps/website/docs/api-reference/commandkit/types/resolvable-command.mdx +++ b/apps/website/docs/api-reference/commandkit/types/resolvable-command.mdx @@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## ResolvableCommand - + Type for commands that can be resolved by the handler. diff --git a/apps/website/docs/api-reference/commandkit/types/run-command.mdx b/apps/website/docs/api-reference/commandkit/types/run-command.mdx index c54fcf6b..f2817cc8 100644 --- a/apps/website/docs/api-reference/commandkit/types/run-command.mdx +++ b/apps/website/docs/api-reference/commandkit/types/run-command.mdx @@ -13,7 +13,7 @@ import MemberDescription from '@site/src/components/MemberDescription'; ## RunCommand - + Function type for wrapping command execution with custom logic. diff --git a/apps/website/docs/api-reference/commandkit/variables/default-mutex.mdx b/apps/website/docs/api-reference/commandkit/variables/default-mutex.mdx new file mode 100644 index 00000000..6771a9fb --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/variables/default-mutex.mdx @@ -0,0 +1,19 @@ +--- +title: "DefaultMutex" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## defaultMutex + + + +Default mutex instance for global use + diff --git a/apps/website/docs/api-reference/commandkit/variables/default-rate-limiter.mdx b/apps/website/docs/api-reference/commandkit/variables/default-rate-limiter.mdx new file mode 100644 index 00000000..30649246 --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/variables/default-rate-limiter.mdx @@ -0,0 +1,19 @@ +--- +title: "DefaultRateLimiter" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## defaultRateLimiter + + + +Default rate limiter instance for global use + diff --git a/apps/website/docs/api-reference/commandkit/variables/default-semaphore.mdx b/apps/website/docs/api-reference/commandkit/variables/default-semaphore.mdx new file mode 100644 index 00000000..1d53d573 --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/variables/default-semaphore.mdx @@ -0,0 +1,19 @@ +--- +title: "DefaultSemaphore" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## defaultSemaphore + + + +Default semaphore instance for global use + diff --git a/apps/website/docs/api-reference/commandkit/variables/default_max_requests.mdx b/apps/website/docs/api-reference/commandkit/variables/default_max_requests.mdx new file mode 100644 index 00000000..373c1b9f --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/variables/default_max_requests.mdx @@ -0,0 +1,19 @@ +--- +title: "DEFAULT_MAX_REQUESTS" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## DEFAULT_MAX_REQUESTS + + + +Default maximum number of requests allowed per interval + diff --git a/apps/website/docs/api-reference/commandkit/variables/default_timeout.mdx b/apps/website/docs/api-reference/commandkit/variables/default_timeout.mdx new file mode 100644 index 00000000..699b7191 --- /dev/null +++ b/apps/website/docs/api-reference/commandkit/variables/default_timeout.mdx @@ -0,0 +1,19 @@ +--- +title: "DEFAULT_TIMEOUT" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## DEFAULT_TIMEOUT + + + +Default timeout interval for rate limiting in milliseconds + diff --git a/apps/website/docs/api-reference/redis/classes/redis-mutex-storage.mdx b/apps/website/docs/api-reference/redis/classes/redis-mutex-storage.mdx new file mode 100644 index 00000000..b2b3cb06 --- /dev/null +++ b/apps/website/docs/api-reference/redis/classes/redis-mutex-storage.mdx @@ -0,0 +1,78 @@ +--- +title: "RedisMutexStorage" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## RedisMutexStorage + + + + + +```ts title="Signature" +class RedisMutexStorage implements MutexStorage { + constructor(redis: Redis) + acquire(key: string, timeout: number = this.defaultTimeout, signal?: AbortSignal) => Promise; + release(key: string) => Promise; + isLocked(key: string) => Promise; + getLockInfo(key: string) => Promise<{ + locked: boolean; + ttl: number; + value?: string; + }>; + forceRelease(key: string) => Promise; + extendLock(key: string, additionalTime: number) => Promise; +} +``` +* Implements: MutexStorage + + + +
+ +### constructor + + RedisMutexStorage`} /> + + +### acquire + + Promise<boolean>`} /> + + +### release + + Promise<void>`} /> + + +### isLocked + + Promise<boolean>`} /> + + +### getLockInfo + + Promise<{ locked: boolean; ttl: number; value?: string; }>`} /> + +Gets information about a lock including its TTL +### forceRelease + + Promise<void>`} /> + +Force releases a lock (use with caution) +### extendLock + + Promise<boolean>`} /> + +Extends the lock timeout + + +
diff --git a/apps/website/docs/api-reference/redis/classes/redis-rate-limit-storage.mdx b/apps/website/docs/api-reference/redis/classes/redis-rate-limit-storage.mdx new file mode 100644 index 00000000..c23e1852 --- /dev/null +++ b/apps/website/docs/api-reference/redis/classes/redis-rate-limit-storage.mdx @@ -0,0 +1,56 @@ +--- +title: "RedisRateLimitStorage" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## RedisRateLimitStorage + + + + + +```ts title="Signature" +class RedisRateLimitStorage implements RateLimitStorage { + constructor(redis: Redis) + get(key: string) => Promise; + set(key: string, value: number) => Promise; + delete(key: string) => Promise; +} +``` +* Implements: RateLimitStorage + + + +
+ +### constructor + + RedisRateLimitStorage`} /> + + +### get + + Promise<number>`} /> + + +### set + + Promise<void>`} /> + + +### delete + + Promise<void>`} /> + + + + +
diff --git a/apps/website/docs/api-reference/redis/classes/redis-semaphore-storage.mdx b/apps/website/docs/api-reference/redis/classes/redis-semaphore-storage.mdx new file mode 100644 index 00000000..a73880b0 --- /dev/null +++ b/apps/website/docs/api-reference/redis/classes/redis-semaphore-storage.mdx @@ -0,0 +1,103 @@ +--- +title: "RedisSemaphoreStorage" +isDefaultIndex: false +generated: true +--- + +import MemberInfo from '@site/src/components/MemberInfo'; +import GenerationInfo from '@site/src/components/GenerationInfo'; +import MemberDescription from '@site/src/components/MemberDescription'; + + + + +## RedisSemaphoreStorage + + + + + +```ts title="Signature" +class RedisSemaphoreStorage implements SemaphoreStorage { + constructor(redis: Redis) + acquire(key: string, timeout: number = this.defaultTimeout, signal?: AbortSignal) => Promise; + release(key: string) => Promise; + getAvailablePermits(key: string) => Promise; + getTotalPermits(key: string) => Promise; + initialize(key: string, permits: number) => Promise; + getSemaphoreInfo(key: string) => Promise<{ + total: number; + available: number; + acquired: number; + initialized: boolean; + }>; + reset(key: string, permits?: number) => Promise; + increasePermits(key: string, additionalPermits: number) => Promise; + decreasePermits(key: string, permitsToRemove: number) => Promise; + clear(key: string) => Promise; +} +``` +* Implements: SemaphoreStorage + + + +
+ +### constructor + + RedisSemaphoreStorage`} /> + + +### acquire + + Promise<boolean>`} /> + + +### release + + Promise<void>`} /> + + +### getAvailablePermits + + Promise<number>`} /> + + +### getTotalPermits + + Promise<number>`} /> + + +### initialize + + Promise<void>`} /> + +Initializes a semaphore with the specified number of permits +### getSemaphoreInfo + + Promise<{ total: number; available: number; acquired: number; initialized: boolean; }>`} /> + +Gets detailed information about a semaphore +### reset + + Promise<void>`} /> + +Resets a semaphore to its initial state +### increasePermits + + Promise<void>`} /> + +Increases the total number of permits for a semaphore +### decreasePermits + + Promise<void>`} /> + +Decreases the total number of permits for a semaphore +### clear + + Promise<void>`} /> + +Clears a semaphore completely + + +
diff --git a/package.json b/package.json index f68e174f..d88d9df6 100644 --- a/package.json +++ b/package.json @@ -76,4 +76,4 @@ "typescript": "^5.8.3", "yaml": "^2.8.0" } -} \ No newline at end of file +} diff --git a/packages/ai/package.json b/packages/ai/package.json index 46839e91..c82fc9d6 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -38,4 +38,4 @@ "ai": "^4.3.16", "zod": "^3.25.48" } -} \ No newline at end of file +} diff --git a/packages/analytics/package.json b/packages/analytics/package.json index 1321cc10..eacc52e0 100644 --- a/packages/analytics/package.json +++ b/packages/analytics/package.json @@ -43,4 +43,4 @@ "tsconfig": "workspace:*", "typescript": "^5.7.3" } -} \ No newline at end of file +} diff --git a/packages/cache/package.json b/packages/cache/package.json index 1a482673..3455ac87 100644 --- a/packages/cache/package.json +++ b/packages/cache/package.json @@ -41,4 +41,4 @@ "ms": "^2.1.3", "stable-hash": "^0.0.6" } -} \ No newline at end of file +} diff --git a/packages/commandkit/async-queue.cjs b/packages/commandkit/async-queue.cjs index 8f721ba2..c287d1cf 100644 --- a/packages/commandkit/async-queue.cjs +++ b/packages/commandkit/async-queue.cjs @@ -1,9 +1,9 @@ const { - AsyncQueue, - createAsyncQueue, + AsyncQueue, + createAsyncQueue, } = require('./dist/utils/useful-stuff/async-queue.js'); module.exports = { - AsyncQueue, - createAsyncQueue, -}; \ No newline at end of file + AsyncQueue, + createAsyncQueue, +}; diff --git a/packages/commandkit/env.cjs b/packages/commandkit/env.cjs index 982f2c85..3976720b 100644 --- a/packages/commandkit/env.cjs +++ b/packages/commandkit/env.cjs @@ -1,13 +1,13 @@ const { - COMMANDKIT_BOOTSTRAP_MODE, - COMMANDKIT_IS_CLI, - COMMANDKIT_IS_DEV, - COMMANDKIT_IS_TEST, + COMMANDKIT_BOOTSTRAP_MODE, + COMMANDKIT_IS_CLI, + COMMANDKIT_IS_DEV, + COMMANDKIT_IS_TEST, } = require('commandkit'); module.exports = { - COMMANDKIT_BOOTSTRAP_MODE, - COMMANDKIT_IS_CLI, - COMMANDKIT_IS_DEV, - COMMANDKIT_IS_TEST, + COMMANDKIT_BOOTSTRAP_MODE, + COMMANDKIT_IS_CLI, + COMMANDKIT_IS_DEV, + COMMANDKIT_IS_TEST, }; diff --git a/packages/commandkit/mutex.cjs b/packages/commandkit/mutex.cjs index b3f74534..333ffde4 100644 --- a/packages/commandkit/mutex.cjs +++ b/packages/commandkit/mutex.cjs @@ -1,21 +1,21 @@ const { - MemoryMutexStorage, - Mutex, - acquireLock, - createMutex, - defaultMutex, - isLocked, - releaseLock, - withMutex, + MemoryMutexStorage, + Mutex, + acquireLock, + createMutex, + defaultMutex, + isLocked, + releaseLock, + withMutex, } = require('./dist/utils/useful-stuff/mutex.js'); module.exports = { - MemoryMutexStorage, - Mutex, - acquireLock, - createMutex, - defaultMutex, - isLocked, - releaseLock, - withMutex, -}; \ No newline at end of file + MemoryMutexStorage, + Mutex, + acquireLock, + createMutex, + defaultMutex, + isLocked, + releaseLock, + withMutex, +}; diff --git a/packages/commandkit/package.json b/packages/commandkit/package.json index 101a7f26..2c1e129b 100644 --- a/packages/commandkit/package.json +++ b/packages/commandkit/package.json @@ -186,4 +186,4 @@ "engines": { "node": ">=22" } -} \ No newline at end of file +} diff --git a/packages/commandkit/ratelimit.cjs b/packages/commandkit/ratelimit.cjs index e034a001..e5c5df83 100644 --- a/packages/commandkit/ratelimit.cjs +++ b/packages/commandkit/ratelimit.cjs @@ -1,26 +1,26 @@ const { - DEFAULT_MAX_REQUESTS, - DEFAULT_TIMEOUT, - MemoryRateLimitStorage, - ratelimit, - RateLimiter, - createRateLimiter, - defaultRateLimiter, - getRemainingRequests, - getResetTime, - resetRateLimit, + DEFAULT_MAX_REQUESTS, + DEFAULT_TIMEOUT, + MemoryRateLimitStorage, + ratelimit, + RateLimiter, + createRateLimiter, + defaultRateLimiter, + getRemainingRequests, + getResetTime, + resetRateLimit, } = require('./dist/utils/useful-stuff/ratelimiter.js'); module.exports = { - RateLimiter, - createRateLimiter, - defaultRateLimiter, - getRemainingRequests, - getResetTime, - resetRateLimit, - RateLimitStorage, - DEFAULT_MAX_REQUESTS, - DEFAULT_TIMEOUT, - MemoryRateLimitStorage, - ratelimit, -}; \ No newline at end of file + RateLimiter, + createRateLimiter, + defaultRateLimiter, + getRemainingRequests, + getResetTime, + resetRateLimit, + RateLimitStorage, + DEFAULT_MAX_REQUESTS, + DEFAULT_TIMEOUT, + MemoryRateLimitStorage, + ratelimit, +}; diff --git a/packages/commandkit/semaphore.cjs b/packages/commandkit/semaphore.cjs index 36c7cca4..28c8fc49 100644 --- a/packages/commandkit/semaphore.cjs +++ b/packages/commandkit/semaphore.cjs @@ -1,23 +1,23 @@ const { - MemorySemaphoreStorage, - Semaphore, - acquirePermit, - createSemaphore, - defaultSemaphore, - getAcquiredPermits, - getAvailablePermits, - releasePermit, - withPermit, + MemorySemaphoreStorage, + Semaphore, + acquirePermit, + createSemaphore, + defaultSemaphore, + getAcquiredPermits, + getAvailablePermits, + releasePermit, + withPermit, } = require('./dist/utils/useful-stuff/semaphore.js'); module.exports = { - MemorySemaphoreStorage, - Semaphore, - acquirePermit, - createSemaphore, - defaultSemaphore, - getAcquiredPermits, - getAvailablePermits, - releasePermit, - withPermit, -}; \ No newline at end of file + MemorySemaphoreStorage, + Semaphore, + acquirePermit, + createSemaphore, + defaultSemaphore, + getAcquiredPermits, + getAvailablePermits, + releasePermit, + withPermit, +}; diff --git a/packages/create-commandkit/package.json b/packages/create-commandkit/package.json index 68315ddb..57141ef1 100644 --- a/packages/create-commandkit/package.json +++ b/packages/create-commandkit/package.json @@ -47,4 +47,4 @@ "tsconfig": "workspace:*", "typescript": "^5.3.3" } -} \ No newline at end of file +} diff --git a/packages/devtools/package.json b/packages/devtools/package.json index 7225802e..de3f052f 100644 --- a/packages/devtools/package.json +++ b/packages/devtools/package.json @@ -29,4 +29,4 @@ "express": "^5.0.0", "jsonwebtoken": "^9.0.2" } -} \ No newline at end of file +} diff --git a/packages/i18n/package.json b/packages/i18n/package.json index 44dfd1c3..55a431dd 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -39,4 +39,4 @@ "tsconfig": "workspace:*", "typescript": "^5.7.3" } -} \ No newline at end of file +} diff --git a/packages/legacy/package.json b/packages/legacy/package.json index 1c3313f7..bfe6b048 100644 --- a/packages/legacy/package.json +++ b/packages/legacy/package.json @@ -34,4 +34,4 @@ "tsconfig": "workspace:*", "typescript": "^5.7.3" } -} \ No newline at end of file +}