|
1 | | -import { Cause, Effect, Exit, FiberMap, Layer, ManagedRuntime, Scope, type Tracer } from 'effect'; |
| 1 | +import { Cause, Effect, Exit, FiberMap, Layer, ManagedRuntime, Scope, Semaphore, type Tracer } from 'effect'; |
2 | 2 | import { CdpSessionId } from '../shared/cloudflare-detection.js'; |
3 | 3 | import type { TargetId, CloudflareConfig } from '../shared/cloudflare-detection.js'; |
4 | 4 | import { CloudflareDetector } from './cf/cloudflare-detector.js'; |
@@ -150,13 +150,20 @@ export class CloudflareSolver { |
150 | 150 |
|
151 | 151 | const proxyOrDirect: SendCommand = (...args) => (self.sendViaProxy || sendCommand)(...args); |
152 | 152 |
|
153 | | - const cdpSenderLayer = Layer.succeed(CdpSender, CdpSender.of({ |
154 | | - send: (method, params, sessionId, timeoutMs) => |
155 | | - liftSend(sendCommand, method, params, sessionId, timeoutMs), |
156 | | - sendViaProxy: (method, params, sessionId, timeoutMs) => |
157 | | - liftSend(proxyOrDirect, method, params, sessionId, timeoutMs), |
158 | | - sendViaBrowser: (method, params, sessionId, timeoutMs) => |
159 | | - liftSend(proxyOrDirect, method, params, sessionId, timeoutMs), |
| 153 | + // Semaphore limits concurrent CDP commands to Chrome — prevents backpressure |
| 154 | + // when multiple tabs have active detection/solve loops firing simultaneously. |
| 155 | + const CDP_CONCURRENCY = 3; |
| 156 | + const cdpSenderLayer = Layer.effect(CdpSender, Effect.gen(function*() { |
| 157 | + const sem = yield* Semaphore.make(CDP_CONCURRENCY); |
| 158 | + const throttle = <A, E>(effect: Effect.Effect<A, E>) => sem.withPermits(1)(effect); |
| 159 | + return CdpSender.of({ |
| 160 | + send: (method, params, sessionId, timeoutMs) => |
| 161 | + throttle(liftSend(sendCommand, method, params, sessionId, timeoutMs)), |
| 162 | + sendViaProxy: (method, params, sessionId, timeoutMs) => |
| 163 | + throttle(liftSend(proxyOrDirect, method, params, sessionId, timeoutMs)), |
| 164 | + sendViaBrowser: (method, params, sessionId, timeoutMs) => |
| 165 | + throttle(liftSend(proxyOrDirect, method, params, sessionId, timeoutMs)), |
| 166 | + }); |
160 | 167 | })); |
161 | 168 |
|
162 | 169 | const solverEventsLayer = Layer.succeed(SolverEvents, SolverEvents.of({ |
@@ -196,8 +203,8 @@ export class CloudflareSolver { |
196 | 203 | })); |
197 | 204 |
|
198 | 205 | // SolveDispatcher — routes solve attempts through the Effect solver. |
199 | | - // Per-solve isolated WS: each solve gets its own WebSocket to Chrome, |
200 | | - // Each solve gets its own isolated WS connection, so no concurrency limit needed. |
| 206 | + // Per-solve isolated WS: each solve gets its own WebSocket to Chrome. |
| 207 | + // Browser-level sends (originalSender) inherit the Semaphore from cdpSenderLayer. |
201 | 208 | const solveDispatcherLayer = Layer.effect(SolveDispatcher, Effect.gen(function*() { |
202 | 209 | const solverEvents = yield* SolverEvents; |
203 | 210 | const solveDeps = yield* SolveDeps; |
@@ -411,7 +418,7 @@ export class CloudflareSolver { |
411 | 418 | } |
412 | 419 |
|
413 | 420 | // FiberMap.run auto-interrupts existing fiber for same key. |
414 | | - // The detection effect is wrapped in catchAllCause to prevent silent fiber |
| 421 | + // The detection effect is wrapped in catchCause to prevent silent fiber |
415 | 422 | // death — without this, defects (NPE in emitClientEvent, etc.) kill the fiber |
416 | 423 | // and pydoll never receives cf.solved/cf.failed (the "events=1" failure mode). |
417 | 424 | const guarded = this.detector.detectTurnstileWidgetEffect(targetId, cdpSessionId).pipe( |
|
0 commit comments