@casys/mcp-server 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Request Queue with Concurrency Control and Backpressure
3
+ *
4
+ * Limits the number of concurrent requests to prevent resource exhaustion
5
+ * and implements backpressure strategies when at capacity.
6
+ *
7
+ * @module lib/server/request-queue
8
+ */
9
+
10
+ import type { QueueMetrics, QueueOptions } from "../types.js";
11
+
12
+ /**
13
+ * RequestQueue manages concurrent request execution with backpressure
14
+ *
15
+ * Features:
16
+ * - Limits max concurrent requests (default: 10)
17
+ * - Multiple backpressure strategies: sleep, queue, reject
18
+ * - Metrics for monitoring (inFlight, queued)
19
+ * - Graceful degradation under load
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * const queue = new RequestQueue({
24
+ * maxConcurrent: 5,
25
+ * strategy: 'queue',
26
+ * sleepMs: 10
27
+ * });
28
+ *
29
+ * await queue.acquire();
30
+ * try {
31
+ * // Execute request
32
+ * } finally {
33
+ * queue.release();
34
+ * }
35
+ * ```
36
+ */
37
+ export class RequestQueue {
38
+ private inFlight = 0;
39
+ private maxConcurrent: number;
40
+ private strategy: "sleep" | "queue" | "reject";
41
+ private sleepMs: number;
42
+ private waitQueue: Array<() => void> = [];
43
+
44
+ constructor(options: QueueOptions) {
45
+ this.maxConcurrent = options.maxConcurrent;
46
+ this.strategy = options.strategy;
47
+ this.sleepMs = options.sleepMs;
48
+ }
49
+
50
+ /**
51
+ * Acquire a slot for request execution
52
+ * Blocks until a slot is available based on backpressure strategy
53
+ *
54
+ * @throws {Error} If strategy is 'reject' and queue is at capacity
55
+ */
56
+ async acquire(): Promise<void> {
57
+ if (this.strategy === "reject") {
58
+ // Fail fast - reject immediately if at capacity
59
+ if (this.inFlight >= this.maxConcurrent) {
60
+ throw new Error(
61
+ `Server at capacity (${this.maxConcurrent} concurrent requests)`,
62
+ );
63
+ }
64
+ this.inFlight++;
65
+ return;
66
+ }
67
+
68
+ if (this.strategy === "queue") {
69
+ // Queue-based backpressure - wait in FIFO queue.
70
+ // Claim the slot BEFORE awaiting to prevent TOCTOU race:
71
+ // without this, two waiters woken back-to-back could both see
72
+ // inFlight < maxConcurrent and both increment past the limit.
73
+ if (this.inFlight >= this.maxConcurrent) {
74
+ await new Promise<void>((resolve) => {
75
+ this.waitQueue.push(resolve);
76
+ });
77
+ // Slot was pre-claimed by release() before waking us
78
+ } else {
79
+ this.inFlight++;
80
+ }
81
+ return;
82
+ }
83
+
84
+ // Sleep-based backpressure (default) - busy-wait with sleep.
85
+ // The check + increment is safe here because JS is single-threaded
86
+ // for synchronous code: after the while loop exits, no other code
87
+ // runs before inFlight++ (no await between check and increment).
88
+ while (this.inFlight >= this.maxConcurrent) {
89
+ await new Promise((resolve) => setTimeout(resolve, this.sleepMs));
90
+ }
91
+ this.inFlight++;
92
+ }
93
+
94
+ /**
95
+ * Release a slot after request completion
96
+ * Notifies next waiting request if using queue strategy
97
+ */
98
+ release(): void {
99
+ if (this.strategy === "queue" && this.waitQueue.length > 0) {
100
+ // Transfer the slot directly to the next waiter without decrementing.
101
+ // This prevents the TOCTOU race: the waiter wakes up with
102
+ // the slot already claimed (inFlight unchanged).
103
+ const next = this.waitQueue.shift()!;
104
+ next();
105
+ } else {
106
+ this.inFlight--;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Get current queue metrics for monitoring
112
+ */
113
+ getMetrics(): QueueMetrics {
114
+ return {
115
+ inFlight: this.inFlight,
116
+ queued: this.waitQueue.length,
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Check if queue is at capacity
122
+ */
123
+ isAtCapacity(): boolean {
124
+ return this.inFlight >= this.maxConcurrent;
125
+ }
126
+
127
+ /**
128
+ * Get current in-flight request count
129
+ */
130
+ getInFlight(): number {
131
+ return this.inFlight;
132
+ }
133
+
134
+ /**
135
+ * Get current queued request count
136
+ */
137
+ getQueued(): number {
138
+ return this.waitQueue.length;
139
+ }
140
+ }