@detiq/crawler 0.1.3
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.
- package/Dockerfile +49 -0
- package/README.md +11 -0
- package/dist/accessibility-auditor.d.ts +29 -0
- package/dist/accessibility-auditor.js +57 -0
- package/dist/accessibility-auditor.js.map +7 -0
- package/dist/ai-login.d.ts +32 -0
- package/dist/ai-login.js +177 -0
- package/dist/ai-login.js.map +7 -0
- package/dist/auth-token-rotator.d.ts +47 -0
- package/dist/auth-token-rotator.js +58 -0
- package/dist/auth-token-rotator.js.map +7 -0
- package/dist/auth.d.ts +20 -0
- package/dist/auth.js +329 -0
- package/dist/auth.js.map +7 -0
- package/dist/browser-extension.d.ts +37 -0
- package/dist/browser-extension.js +113 -0
- package/dist/browser-extension.js.map +7 -0
- package/dist/captcha-solver.d.ts +16 -0
- package/dist/captcha-solver.js +105 -0
- package/dist/captcha-solver.js.map +7 -0
- package/dist/color-contrast.d.ts +30 -0
- package/dist/color-contrast.js +78 -0
- package/dist/color-contrast.js.map +7 -0
- package/dist/content-chunker.d.ts +18 -0
- package/dist/content-chunker.js +152 -0
- package/dist/content-chunker.js.map +7 -0
- package/dist/content-dedup.d.ts +23 -0
- package/dist/content-dedup.js +60 -0
- package/dist/content-dedup.js.map +7 -0
- package/dist/cookie-consent.d.ts +25 -0
- package/dist/cookie-consent.js +78 -0
- package/dist/cookie-consent.js.map +7 -0
- package/dist/cookie-jar.d.ts +20 -0
- package/dist/cookie-jar.js +61 -0
- package/dist/cookie-jar.js.map +7 -0
- package/dist/crawler.d.ts +179 -0
- package/dist/crawler.js +3672 -0
- package/dist/crawler.js.map +7 -0
- package/dist/custom-js-injector.d.ts +42 -0
- package/dist/custom-js-injector.js +50 -0
- package/dist/custom-js-injector.js.map +7 -0
- package/dist/dark-mode-capture.d.ts +23 -0
- package/dist/dark-mode-capture.js +75 -0
- package/dist/dark-mode-capture.js.map +7 -0
- package/dist/dead-link-detector.d.ts +37 -0
- package/dist/dead-link-detector.js +50 -0
- package/dist/dead-link-detector.js.map +7 -0
- package/dist/element-hash.d.ts +9 -0
- package/dist/element-hash.js +9 -0
- package/dist/element-hash.js.map +7 -0
- package/dist/email-otp.d.ts +42 -0
- package/dist/email-otp.js +177 -0
- package/dist/email-otp.js.map +7 -0
- package/dist/engine-router.d.ts +47 -0
- package/dist/engine-router.js +94 -0
- package/dist/engine-router.js.map +7 -0
- package/dist/export/bulk-export.d.ts +26 -0
- package/dist/export/bulk-export.js +76 -0
- package/dist/export/bulk-export.js.map +7 -0
- package/dist/export/vector-export.d.ts +76 -0
- package/dist/export/vector-export.js +85 -0
- package/dist/export/vector-export.js.map +7 -0
- package/dist/extractors/advanced.d.ts +54 -0
- package/dist/extractors/advanced.js +245 -0
- package/dist/extractors/advanced.js.map +7 -0
- package/dist/extractors/artifact-envelope.d.ts +63 -0
- package/dist/extractors/artifact-envelope.js +63 -0
- package/dist/extractors/artifact-envelope.js.map +7 -0
- package/dist/failure-analysis-agent.d.ts +16 -0
- package/dist/failure-analysis-agent.js +40 -0
- package/dist/failure-analysis-agent.js.map +7 -0
- package/dist/feature-flag-detector.d.ts +30 -0
- package/dist/feature-flag-detector.js +55 -0
- package/dist/feature-flag-detector.js.map +7 -0
- package/dist/form-detector.d.ts +34 -0
- package/dist/form-detector.js +58 -0
- package/dist/form-detector.js.map +7 -0
- package/dist/frontier.d.ts +32 -0
- package/dist/frontier.js +83 -0
- package/dist/frontier.js.map +7 -0
- package/dist/gap-pass.d.ts +35 -0
- package/dist/gap-pass.js +81 -0
- package/dist/gap-pass.js.map +7 -0
- package/dist/graphql-detector.d.ts +39 -0
- package/dist/graphql-detector.js +86 -0
- package/dist/graphql-detector.js.map +7 -0
- package/dist/http-engine.d.ts +80 -0
- package/dist/http-engine.js +304 -0
- package/dist/http-engine.js.map +7 -0
- package/dist/incremental-diff.d.ts +59 -0
- package/dist/incremental-diff.js +61 -0
- package/dist/incremental-diff.js.map +7 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +61 -0
- package/dist/index.js.map +7 -0
- package/dist/input-fuzzer.d.ts +30 -0
- package/dist/input-fuzzer.js +105 -0
- package/dist/input-fuzzer.js.map +7 -0
- package/dist/interactive-probe.d.ts +104 -0
- package/dist/interactive-probe.js +442 -0
- package/dist/interactive-probe.js.map +7 -0
- package/dist/job-queue.d.ts +171 -0
- package/dist/job-queue.js +2256 -0
- package/dist/job-queue.js.map +7 -0
- package/dist/js-coverage.d.ts +29 -0
- package/dist/js-coverage.js +56 -0
- package/dist/js-coverage.js.map +7 -0
- package/dist/js-router-detector.d.ts +17 -0
- package/dist/js-router-detector.js +90 -0
- package/dist/js-router-detector.js.map +7 -0
- package/dist/language-detector.d.ts +22 -0
- package/dist/language-detector.js +57 -0
- package/dist/language-detector.js.map +7 -0
- package/dist/link-classifier.d.ts +23 -0
- package/dist/link-classifier.js +40 -0
- package/dist/link-classifier.js.map +7 -0
- package/dist/live-view.d.ts +36 -0
- package/dist/live-view.js +100 -0
- package/dist/live-view.js.map +7 -0
- package/dist/logger.d.ts +2 -0
- package/dist/logger.js +10 -0
- package/dist/logger.js.map +7 -0
- package/dist/markdown-extract.d.ts +8 -0
- package/dist/markdown-extract.js +54 -0
- package/dist/markdown-extract.js.map +7 -0
- package/dist/native-crawler.d.ts +69 -0
- package/dist/native-crawler.js +839 -0
- package/dist/native-crawler.js.map +7 -0
- package/dist/network-observer.d.ts +49 -0
- package/dist/network-observer.js +149 -0
- package/dist/network-observer.js.map +7 -0
- package/dist/network-throttle.d.ts +25 -0
- package/dist/network-throttle.js +34 -0
- package/dist/network-throttle.js.map +7 -0
- package/dist/openapi-generator.d.ts +49 -0
- package/dist/openapi-generator.js +89 -0
- package/dist/openapi-generator.js.map +7 -0
- package/dist/pdf-extractor.d.ts +28 -0
- package/dist/pdf-extractor.js +81 -0
- package/dist/pdf-extractor.js.map +7 -0
- package/dist/performance-metrics.d.ts +53 -0
- package/dist/performance-metrics.js +83 -0
- package/dist/performance-metrics.js.map +7 -0
- package/dist/phash.d.ts +38 -0
- package/dist/phash.js +115 -0
- package/dist/phash.js.map +7 -0
- package/dist/pii-scrubber.d.ts +21 -0
- package/dist/pii-scrubber.js +75 -0
- package/dist/pii-scrubber.js.map +7 -0
- package/dist/playwright-recorder.d.ts +39 -0
- package/dist/playwright-recorder.js +123 -0
- package/dist/playwright-recorder.js.map +7 -0
- package/dist/policies/crawl-safety.d.ts +9 -0
- package/dist/policies/crawl-safety.js +37 -0
- package/dist/policies/crawl-safety.js.map +7 -0
- package/dist/profiles/domain-profile.d.ts +19 -0
- package/dist/profiles/domain-profile.js +140 -0
- package/dist/profiles/domain-profile.js.map +7 -0
- package/dist/proxy.d.ts +50 -0
- package/dist/proxy.js +165 -0
- package/dist/proxy.js.map +7 -0
- package/dist/rate-limiter.d.ts +31 -0
- package/dist/rate-limiter.js +64 -0
- package/dist/rate-limiter.js.map +7 -0
- package/dist/redirect-tracker.d.ts +37 -0
- package/dist/redirect-tracker.js +65 -0
- package/dist/redirect-tracker.js.map +7 -0
- package/dist/redis-frontier.d.ts +48 -0
- package/dist/redis-frontier.js +126 -0
- package/dist/redis-frontier.js.map +7 -0
- package/dist/redis-queue.d.ts +48 -0
- package/dist/redis-queue.js +165 -0
- package/dist/redis-queue.js.map +7 -0
- package/dist/response-cache.d.ts +38 -0
- package/dist/response-cache.js +140 -0
- package/dist/response-cache.js.map +7 -0
- package/dist/responsive-detector.d.ts +29 -0
- package/dist/responsive-detector.js +46 -0
- package/dist/responsive-detector.js.map +7 -0
- package/dist/retry-policy.d.ts +20 -0
- package/dist/retry-policy.js +54 -0
- package/dist/retry-policy.js.map +7 -0
- package/dist/robots-txt.d.ts +9 -0
- package/dist/robots-txt.js +77 -0
- package/dist/robots-txt.js.map +7 -0
- package/dist/screen-reader.d.ts +16 -0
- package/dist/screen-reader.js +46 -0
- package/dist/screen-reader.js.map +7 -0
- package/dist/screenshot-masker.d.ts +28 -0
- package/dist/screenshot-masker.js +62 -0
- package/dist/screenshot-masker.js.map +7 -0
- package/dist/seo-checker.d.ts +33 -0
- package/dist/seo-checker.js +91 -0
- package/dist/seo-checker.js.map +7 -0
- package/dist/session-capture.d.ts +35 -0
- package/dist/session-capture.js +267 -0
- package/dist/session-capture.js.map +7 -0
- package/dist/session-export.d.ts +49 -0
- package/dist/session-export.js +117 -0
- package/dist/session-export.js.map +7 -0
- package/dist/sitemap-writer.d.ts +21 -0
- package/dist/sitemap-writer.js +47 -0
- package/dist/sitemap-writer.js.map +7 -0
- package/dist/sitemap.d.ts +51 -0
- package/dist/sitemap.js +152 -0
- package/dist/sitemap.js.map +7 -0
- package/dist/skyvern.d.ts +56 -0
- package/dist/skyvern.js +129 -0
- package/dist/skyvern.js.map +7 -0
- package/dist/sqs-queue.d.ts +2 -0
- package/dist/sqs-queue.js +47 -0
- package/dist/sqs-queue.js.map +7 -0
- package/dist/sso-token.d.ts +29 -0
- package/dist/sso-token.js +76 -0
- package/dist/sso-token.js.map +7 -0
- package/dist/stealth/human-behavior.d.ts +20 -0
- package/dist/stealth/human-behavior.js +168 -0
- package/dist/stealth/human-behavior.js.map +7 -0
- package/dist/telemetry.d.ts +34 -0
- package/dist/telemetry.js +77 -0
- package/dist/telemetry.js.map +7 -0
- package/dist/test-executor.d.ts +67 -0
- package/dist/test-executor.js +311 -0
- package/dist/test-executor.js.map +7 -0
- package/dist/tls-fingerprint.d.ts +50 -0
- package/dist/tls-fingerprint.js +81 -0
- package/dist/tls-fingerprint.js.map +7 -0
- package/dist/url-pattern-completer.d.ts +28 -0
- package/dist/url-pattern-completer.js +87 -0
- package/dist/url-pattern-completer.js.map +7 -0
- package/dist/visual-diff-verdict.d.ts +40 -0
- package/dist/visual-diff-verdict.js +25 -0
- package/dist/visual-diff-verdict.js.map +7 -0
- package/dist/webhook-notifier.d.ts +30 -0
- package/dist/webhook-notifier.js +76 -0
- package/dist/webhook-notifier.js.map +7 -0
- package/dist/websocket-detector.d.ts +24 -0
- package/dist/websocket-detector.js +60 -0
- package/dist/websocket-detector.js.map +7 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +33 -0
- package/dist/worker.js.map +7 -0
- package/package.json +45 -0
- package/src/accessibility-auditor.ts +102 -0
- package/src/ai-login.ts +218 -0
- package/src/auth-token-rotator.ts +89 -0
- package/src/auth.ts +396 -0
- package/src/browser-extension.ts +186 -0
- package/src/captcha-solver.ts +143 -0
- package/src/color-contrast.ts +116 -0
- package/src/content-chunker.ts +170 -0
- package/src/content-dedup.ts +99 -0
- package/src/cookie-consent.ts +106 -0
- package/src/cookie-jar.ts +91 -0
- package/src/crawler.ts +4305 -0
- package/src/custom-js-injector.ts +93 -0
- package/src/dark-mode-capture.ts +110 -0
- package/src/dead-link-detector.ts +76 -0
- package/src/element-hash.ts +16 -0
- package/src/email-otp.ts +252 -0
- package/src/engine-router.ts +141 -0
- package/src/export/bulk-export.ts +85 -0
- package/src/export/vector-export.ts +160 -0
- package/src/extractors/advanced.ts +283 -0
- package/src/extractors/artifact-envelope.ts +111 -0
- package/src/failure-analysis-agent.ts +49 -0
- package/src/feature-flag-detector.ts +97 -0
- package/src/form-detector.ts +103 -0
- package/src/frontier.test.ts +64 -0
- package/src/frontier.ts +112 -0
- package/src/gap-pass.ts +122 -0
- package/src/graphql-detector.ts +129 -0
- package/src/http-engine.ts +433 -0
- package/src/incremental-diff.ts +121 -0
- package/src/index.ts +35 -0
- package/src/input-fuzzer.ts +129 -0
- package/src/interactive-probe.ts +623 -0
- package/src/job-queue.test.ts +120 -0
- package/src/job-queue.ts +2701 -0
- package/src/js-coverage.ts +87 -0
- package/src/js-router-detector.ts +112 -0
- package/src/language-detector.ts +82 -0
- package/src/link-classifier.ts +65 -0
- package/src/live-view.ts +141 -0
- package/src/logger.ts +9 -0
- package/src/markdown-extract.test.ts +73 -0
- package/src/markdown-extract.ts +76 -0
- package/src/native-crawler.ts +1106 -0
- package/src/network-observer.ts +204 -0
- package/src/network-throttle.ts +69 -0
- package/src/openapi-generator.ts +135 -0
- package/src/pdf-extractor.ts +123 -0
- package/src/performance-metrics.ts +134 -0
- package/src/phash.ts +151 -0
- package/src/pii-scrubber.ts +99 -0
- package/src/playwright-recorder.ts +170 -0
- package/src/policies/crawl-safety.ts +46 -0
- package/src/profiles/domain-profile.ts +167 -0
- package/src/proxy.ts +238 -0
- package/src/rate-limiter.ts +87 -0
- package/src/redirect-tracker.ts +97 -0
- package/src/redis-frontier.ts +143 -0
- package/src/redis-queue.ts +208 -0
- package/src/response-cache.ts +150 -0
- package/src/responsive-detector.ts +92 -0
- package/src/retry-policy.ts +87 -0
- package/src/robots-txt.ts +96 -0
- package/src/screen-reader.ts +89 -0
- package/src/screenshot-masker.ts +98 -0
- package/src/seo-checker.ts +134 -0
- package/src/session-capture.ts +325 -0
- package/src/session-export.test.ts +75 -0
- package/src/session-export.ts +172 -0
- package/src/sitemap-writer.ts +65 -0
- package/src/sitemap.test.ts +131 -0
- package/src/sitemap.ts +224 -0
- package/src/skyvern.ts +199 -0
- package/src/sqs-queue.ts +49 -0
- package/src/sso-token.ts +120 -0
- package/src/stealth/human-behavior.ts +191 -0
- package/src/telemetry.ts +122 -0
- package/src/test-executor.test.ts +72 -0
- package/src/test-executor.ts +424 -0
- package/src/tls-fingerprint.ts +111 -0
- package/src/url-pattern-completer.ts +114 -0
- package/src/visual-diff-verdict.test.ts +80 -0
- package/src/visual-diff-verdict.ts +57 -0
- package/src/webhook-notifier.ts +109 -0
- package/src/websocket-detector.ts +83 -0
- package/src/worker.ts +48 -0
- package/tsconfig.json +13 -0
- package/types/accessibility-auditor.d.ts +29 -0
- package/types/ai-login.d.ts +32 -0
- package/types/auth-token-rotator.d.ts +47 -0
- package/types/auth.d.ts +20 -0
- package/types/browser-extension.d.ts +37 -0
- package/types/bull-worker.d.ts +6 -0
- package/types/captcha-solver.d.ts +16 -0
- package/types/color-contrast.d.ts +30 -0
- package/types/content-chunker.d.ts +18 -0
- package/types/content-dedup.d.ts +23 -0
- package/types/cookie-consent.d.ts +25 -0
- package/types/cookie-jar.d.ts +20 -0
- package/types/crawler.d.ts +179 -0
- package/types/custom-js-injector.d.ts +42 -0
- package/types/dark-mode-capture.d.ts +23 -0
- package/types/dead-link-detector.d.ts +37 -0
- package/types/element-hash.d.ts +9 -0
- package/types/email-otp.d.ts +42 -0
- package/types/engine-router.d.ts +47 -0
- package/types/export/bulk-export.d.ts +26 -0
- package/types/export/vector-export.d.ts +76 -0
- package/types/extractors/advanced.d.ts +54 -0
- package/types/extractors/artifact-envelope.d.ts +63 -0
- package/types/failure-analysis-agent.d.ts +16 -0
- package/types/feature-flag-detector.d.ts +30 -0
- package/types/form-detector.d.ts +34 -0
- package/types/frontier.d.ts +32 -0
- package/types/gap-pass.d.ts +35 -0
- package/types/graphql-detector.d.ts +39 -0
- package/types/http-engine.d.ts +80 -0
- package/types/incremental-diff.d.ts +59 -0
- package/types/index.d.ts +35 -0
- package/types/input-fuzzer.d.ts +30 -0
- package/types/interactive-probe.d.ts +104 -0
- package/types/job-queue.d.ts +171 -0
- package/types/js-coverage.d.ts +29 -0
- package/types/js-router-detector.d.ts +17 -0
- package/types/language-detector.d.ts +22 -0
- package/types/link-classifier.d.ts +23 -0
- package/types/live-view.d.ts +36 -0
- package/types/logger.d.ts +2 -0
- package/types/markdown-extract.d.ts +8 -0
- package/types/native-crawler.d.ts +69 -0
- package/types/network-observer.d.ts +49 -0
- package/types/network-throttle.d.ts +25 -0
- package/types/openapi-generator.d.ts +49 -0
- package/types/pdf-extractor.d.ts +28 -0
- package/types/performance-metrics.d.ts +53 -0
- package/types/phash.d.ts +38 -0
- package/types/pii-scrubber.d.ts +21 -0
- package/types/playwright-recorder.d.ts +39 -0
- package/types/policies/crawl-safety.d.ts +9 -0
- package/types/profiles/domain-profile.d.ts +19 -0
- package/types/proxy.d.ts +50 -0
- package/types/rate-limiter.d.ts +31 -0
- package/types/redirect-tracker.d.ts +37 -0
- package/types/redis-frontier.d.ts +48 -0
- package/types/redis-queue.d.ts +48 -0
- package/types/response-cache.d.ts +38 -0
- package/types/responsive-detector.d.ts +29 -0
- package/types/retry-policy.d.ts +20 -0
- package/types/robots-txt.d.ts +9 -0
- package/types/screen-reader.d.ts +16 -0
- package/types/screenshot-masker.d.ts +28 -0
- package/types/seo-checker.d.ts +33 -0
- package/types/session-capture.d.ts +38 -0
- package/types/session-export.d.ts +49 -0
- package/types/sitemap-writer.d.ts +21 -0
- package/types/sitemap.d.ts +51 -0
- package/types/skyvern.d.ts +56 -0
- package/types/sqs-queue.d.ts +2 -0
- package/types/sso-token.d.ts +29 -0
- package/types/stealth/human-behavior.d.ts +20 -0
- package/types/telemetry.d.ts +34 -0
- package/types/test-executor.d.ts +67 -0
- package/types/tls-fingerprint.d.ts +50 -0
- package/types/url-pattern-completer.d.ts +28 -0
- package/types/visual-diff-verdict.d.ts +40 -0
- package/types/webhook-notifier.d.ts +30 -0
- package/types/websocket-detector.d.ts +24 -0
- package/types/worker.d.ts +1 -0
package/src/proxy.ts
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Free proxy auto-selection for bot-protection bypass.
|
|
3
|
+
*
|
|
4
|
+
* Source: proxifly/free-proxy-list (MIT) via jsDelivr CDN.
|
|
5
|
+
* These are datacenter proxies — they bypass simple IP blocks but NOT
|
|
6
|
+
* full Cloudflare Bot Manager / Akamai. For Cloudflare-protected sites,
|
|
7
|
+
* users need a residential proxy (Bright Data, Oxylabs, etc.).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createConnection } from 'net';
|
|
11
|
+
|
|
12
|
+
const PROXY_LIST_URL =
|
|
13
|
+
'https://cdn.jsdelivr.net/gh/proxifly/free-proxy-list@main/proxies/all/data.json';
|
|
14
|
+
|
|
15
|
+
interface ProxyEntry {
|
|
16
|
+
ip: string;
|
|
17
|
+
port: number;
|
|
18
|
+
protocols: string[];
|
|
19
|
+
upTime?: number;
|
|
20
|
+
speed?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Fetch and filter proxy list. Returns http proxies sorted by speed (fastest first). */
|
|
24
|
+
async function fetchCandidates(): Promise<ProxyEntry[]> {
|
|
25
|
+
const res = await fetch(PROXY_LIST_URL, {
|
|
26
|
+
signal: AbortSignal.timeout(10_000),
|
|
27
|
+
headers: { 'Accept': 'application/json' },
|
|
28
|
+
});
|
|
29
|
+
if (!res.ok) throw new Error(`proxy list fetch failed: ${res.status}`);
|
|
30
|
+
const raw: ProxyEntry[] = await res.json();
|
|
31
|
+
return raw
|
|
32
|
+
.filter((p) => p.protocols?.includes('http') && p.port > 0 && (p.upTime ?? 0) >= 30)
|
|
33
|
+
.sort((a, b) => (a.speed ?? 99999) - (b.speed ?? 99999))
|
|
34
|
+
.slice(0, 50); // test at most 50 candidates
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** TCP-level proxy liveness check via HTTP CONNECT tunnel. No extra dependencies. */
|
|
38
|
+
function testProxy(ip: string, port: number, timeoutMs: number): Promise<boolean> {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
let settled = false;
|
|
41
|
+
const settle = (ok: boolean) => {
|
|
42
|
+
if (settled) return;
|
|
43
|
+
settled = true;
|
|
44
|
+
resolve(ok);
|
|
45
|
+
try { sock.destroy(); } catch { /* ignore */ }
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const timer = setTimeout(() => settle(false), timeoutMs);
|
|
49
|
+
const sock = createConnection({ host: ip, port, timeout: timeoutMs });
|
|
50
|
+
|
|
51
|
+
sock.on('connect', () => {
|
|
52
|
+
sock.write('CONNECT example.com:80 HTTP/1.1\r\nHost: example.com:80\r\n\r\n');
|
|
53
|
+
sock.once('data', (data) => {
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
const ok = /^HTTP\/1\.[01] 200/.test(data.toString());
|
|
56
|
+
settle(ok);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
sock.on('error', () => { clearTimeout(timer); settle(false); });
|
|
60
|
+
sock.on('timeout', () => { clearTimeout(timer); settle(false); });
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Test `batchSize` proxies concurrently and return first working one. */
|
|
65
|
+
async function testBatch(
|
|
66
|
+
candidates: ProxyEntry[],
|
|
67
|
+
start: number,
|
|
68
|
+
batchSize: number,
|
|
69
|
+
timeoutMs: number,
|
|
70
|
+
): Promise<string | null> {
|
|
71
|
+
const batch = candidates.slice(start, start + batchSize);
|
|
72
|
+
if (batch.length === 0) return null;
|
|
73
|
+
|
|
74
|
+
const results = await Promise.all(
|
|
75
|
+
batch.map(async (p) => {
|
|
76
|
+
const ok = await testProxy(p.ip, p.port, timeoutMs);
|
|
77
|
+
return ok ? `http://${p.ip}:${p.port}` : null;
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return results.find((r) => r !== null) ?? null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Pick a working free proxy. Tests in parallel batches of 10 with a 4s timeout.
|
|
86
|
+
* Returns null if no working proxy found (not an error — crawl can proceed without proxy).
|
|
87
|
+
*/
|
|
88
|
+
export async function pickFreeProxy(): Promise<string | null> {
|
|
89
|
+
let candidates: ProxyEntry[];
|
|
90
|
+
try {
|
|
91
|
+
candidates = await fetchCandidates();
|
|
92
|
+
} catch (err: any) {
|
|
93
|
+
console.warn('[proxy] failed to fetch proxy list:', err?.message ?? err);
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const BATCH = 10;
|
|
98
|
+
const TIMEOUT = 4_000;
|
|
99
|
+
|
|
100
|
+
for (let i = 0; i < candidates.length; i += BATCH) {
|
|
101
|
+
const found = await testBatch(candidates, i, BATCH, TIMEOUT);
|
|
102
|
+
if (found) {
|
|
103
|
+
console.info(`[proxy] selected free proxy: ${found} (tested ${i + BATCH} candidates)`);
|
|
104
|
+
return found;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.warn('[proxy] no working free proxy found in list');
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Tiered Proxy Rotation ─────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
export type ProxyTier = 'free' | 'datacenter' | 'residential' | 'premium';
|
|
115
|
+
|
|
116
|
+
export interface ProxyTierConfig {
|
|
117
|
+
/** List of proxy URLs for this tier. E.g. ['http://user:pass@dc1.proxy:8080'] */
|
|
118
|
+
urls: string[];
|
|
119
|
+
tier: ProxyTier;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface DomainHistory {
|
|
123
|
+
tier: ProxyTier;
|
|
124
|
+
consecutiveFailures: number;
|
|
125
|
+
lastEscalatedAt: number;
|
|
126
|
+
totalRequests: number;
|
|
127
|
+
successRate: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const TIER_ORDER: ProxyTier[] = ['free', 'datacenter', 'residential', 'premium'];
|
|
131
|
+
|
|
132
|
+
function nextTier(current: ProxyTier): ProxyTier {
|
|
133
|
+
const idx = TIER_ORDER.indexOf(current);
|
|
134
|
+
return TIER_ORDER[Math.min(idx + 1, TIER_ORDER.length - 1)];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Tiered proxy manager with per-domain error histograms.
|
|
139
|
+
*
|
|
140
|
+
* Tracks success/failure per domain and automatically escalates to higher proxy tiers
|
|
141
|
+
* on consecutive failures. Applies time-decay to probe returning to lower tiers.
|
|
142
|
+
*
|
|
143
|
+
* Usage:
|
|
144
|
+
* const mgr = new TieredProxyManager([{ tier: 'datacenter', urls: [...] }]);
|
|
145
|
+
* const proxy = await mgr.selectProxy('api.salesforce.com');
|
|
146
|
+
* // ... make request ...
|
|
147
|
+
* mgr.recordResult('api.salesforce.com', success);
|
|
148
|
+
*/
|
|
149
|
+
export class TieredProxyManager {
|
|
150
|
+
private tiers: Map<ProxyTier, string[]> = new Map();
|
|
151
|
+
private domainHistory: Map<string, DomainHistory> = new Map();
|
|
152
|
+
private readonly ESCALATE_AFTER = 3; // consecutive failures before escalation
|
|
153
|
+
private readonly DECAY_AFTER_MS = 30 * 60 * 1000; // 30 min: probe tier downgrade
|
|
154
|
+
|
|
155
|
+
constructor(configs: ProxyTierConfig[]) {
|
|
156
|
+
for (const cfg of configs) {
|
|
157
|
+
if (cfg.urls.length > 0) this.tiers.set(cfg.tier, cfg.urls);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private getHistory(domain: string): DomainHistory {
|
|
162
|
+
if (!this.domainHistory.has(domain)) {
|
|
163
|
+
this.domainHistory.set(domain, {
|
|
164
|
+
tier: this.lowestAvailableTier(),
|
|
165
|
+
consecutiveFailures: 0,
|
|
166
|
+
lastEscalatedAt: 0,
|
|
167
|
+
totalRequests: 0,
|
|
168
|
+
successRate: 1,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return this.domainHistory.get(domain)!;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private lowestAvailableTier(): ProxyTier {
|
|
175
|
+
for (const t of TIER_ORDER) {
|
|
176
|
+
if (this.tiers.has(t)) return t;
|
|
177
|
+
}
|
|
178
|
+
return 'free';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Pick a proxy URL for this domain, based on its tier history. */
|
|
182
|
+
selectProxy(domain: string): string | null {
|
|
183
|
+
const history = this.getHistory(domain);
|
|
184
|
+
|
|
185
|
+
// Time-decay: if we've been on elevated tier for DECAY_AFTER_MS, try stepping down
|
|
186
|
+
if (history.consecutiveFailures === 0 && history.lastEscalatedAt > 0) {
|
|
187
|
+
const elapsed = Date.now() - history.lastEscalatedAt;
|
|
188
|
+
if (elapsed > this.DECAY_AFTER_MS) {
|
|
189
|
+
const currentIdx = TIER_ORDER.indexOf(history.tier);
|
|
190
|
+
if (currentIdx > 0) {
|
|
191
|
+
const lowerTier = TIER_ORDER[currentIdx - 1];
|
|
192
|
+
if (this.tiers.has(lowerTier)) {
|
|
193
|
+
history.tier = lowerTier;
|
|
194
|
+
history.lastEscalatedAt = 0;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const urls = this.tiers.get(history.tier);
|
|
201
|
+
if (!urls || urls.length === 0) return null;
|
|
202
|
+
// Round-robin within tier
|
|
203
|
+
const idx = history.totalRequests % urls.length;
|
|
204
|
+
return urls[idx];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Record a request result for a domain. Triggers tier escalation on repeated failures. */
|
|
208
|
+
recordResult(domain: string, success: boolean): void {
|
|
209
|
+
const history = this.getHistory(domain);
|
|
210
|
+
history.totalRequests++;
|
|
211
|
+
if (success) {
|
|
212
|
+
history.consecutiveFailures = 0;
|
|
213
|
+
// Update EMA success rate
|
|
214
|
+
history.successRate = history.successRate * 0.9 + 0.1;
|
|
215
|
+
} else {
|
|
216
|
+
history.consecutiveFailures++;
|
|
217
|
+
history.successRate = history.successRate * 0.9;
|
|
218
|
+
if (history.consecutiveFailures >= this.ESCALATE_AFTER) {
|
|
219
|
+
const elevated = nextTier(history.tier);
|
|
220
|
+
if (elevated !== history.tier && this.tiers.has(elevated)) {
|
|
221
|
+
console.warn(`[proxy-tier] domain=${domain} escalating ${history.tier}→${elevated} after ${history.consecutiveFailures} failures`);
|
|
222
|
+
history.tier = elevated;
|
|
223
|
+
history.lastEscalatedAt = Date.now();
|
|
224
|
+
history.consecutiveFailures = 0;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
stats(): Array<{ domain: string; tier: ProxyTier; successRate: number; totalRequests: number }> {
|
|
231
|
+
return Array.from(this.domainHistory.entries()).map(([domain, h]) => ({
|
|
232
|
+
domain,
|
|
233
|
+
tier: h.tier,
|
|
234
|
+
successRate: Math.round(h.successRate * 100) / 100,
|
|
235
|
+
totalRequests: h.totalRequests,
|
|
236
|
+
}));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain-aware rate limiter — ensures the crawler respects per-domain rate limits.
|
|
3
|
+
*
|
|
4
|
+
* Uses a token bucket algorithm per domain. When the bucket is empty, requests
|
|
5
|
+
* wait until a token is available. Tokens refill at a configurable rate.
|
|
6
|
+
*
|
|
7
|
+
* Default: 2 requests/second per domain, burst of 5.
|
|
8
|
+
* Can be configured per-domain via the rateMap (domain → requests/second).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface RateLimiterOpts {
|
|
12
|
+
defaultRps?: number; // requests per second (default 2)
|
|
13
|
+
burstSize?: number; // max burst (default 5)
|
|
14
|
+
rateMap?: Record<string, number>; // domain-specific overrides
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface Bucket {
|
|
18
|
+
tokens: number;
|
|
19
|
+
lastRefill: number; // Date.now()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function extractDomain(url: string): string {
|
|
23
|
+
try { return new URL(url).hostname; } catch { return url; }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class DomainRateLimiter {
|
|
27
|
+
private buckets = new Map<string, Bucket>();
|
|
28
|
+
private readonly defaultRps: number;
|
|
29
|
+
private readonly burstSize: number;
|
|
30
|
+
private readonly rateMap: Record<string, number>;
|
|
31
|
+
|
|
32
|
+
constructor(opts: RateLimiterOpts = {}) {
|
|
33
|
+
this.defaultRps = opts.defaultRps ?? 2;
|
|
34
|
+
this.burstSize = opts.burstSize ?? 5;
|
|
35
|
+
this.rateMap = opts.rateMap ?? {};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private getBucket(domain: string): Bucket {
|
|
39
|
+
if (!this.buckets.has(domain)) {
|
|
40
|
+
this.buckets.set(domain, { tokens: this.burstSize, lastRefill: Date.now() });
|
|
41
|
+
}
|
|
42
|
+
return this.buckets.get(domain)!;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private refill(domain: string, bucket: Bucket): void {
|
|
46
|
+
const rps = this.rateMap[domain] ?? this.defaultRps;
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
const elapsed = (now - bucket.lastRefill) / 1000;
|
|
49
|
+
bucket.tokens = Math.min(this.burstSize, bucket.tokens + elapsed * rps);
|
|
50
|
+
bucket.lastRefill = now;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Wait until a token is available for the given URL's domain. */
|
|
54
|
+
async throttle(url: string): Promise<void> {
|
|
55
|
+
const domain = extractDomain(url);
|
|
56
|
+
const bucket = this.getBucket(domain);
|
|
57
|
+
this.refill(domain, bucket);
|
|
58
|
+
|
|
59
|
+
if (bucket.tokens >= 1) {
|
|
60
|
+
bucket.tokens -= 1;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Calculate wait time until next token
|
|
65
|
+
const rps = this.rateMap[domain] ?? this.defaultRps;
|
|
66
|
+
const waitMs = Math.ceil((1 - bucket.tokens) / rps * 1000);
|
|
67
|
+
await new Promise(r => setTimeout(r, waitMs));
|
|
68
|
+
bucket.tokens = 0;
|
|
69
|
+
bucket.lastRefill = Date.now();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Override rate for a specific domain at runtime (e.g. after seeing 429). */
|
|
73
|
+
setRate(domain: string, rps: number): void {
|
|
74
|
+
this.rateMap[domain] = rps;
|
|
75
|
+
const bucket = this.getBucket(domain);
|
|
76
|
+
bucket.tokens = 0; // drain immediately on rate change
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
stats(): Record<string, { tokens: number; rps: number }> {
|
|
80
|
+
const result: Record<string, any> = {};
|
|
81
|
+
for (const [domain, bucket] of this.buckets) {
|
|
82
|
+
this.refill(domain, bucket);
|
|
83
|
+
result[domain] = { tokens: Math.round(bucket.tokens * 100) / 100, rps: this.rateMap[domain] ?? this.defaultRps };
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redirect chain tracker — records the full redirect chain for crawled URLs.
|
|
3
|
+
*
|
|
4
|
+
* Why: redirect chains (A→B→C) slow down pages; circular redirects crash crawlers;
|
|
5
|
+
* redirect loops indicate config errors. ZeTa records chains for SEO/perf audit.
|
|
6
|
+
*
|
|
7
|
+
* Integration: hook into Playwright's page.on('response') to see each redirect hop.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface RedirectHop {
|
|
11
|
+
from: string;
|
|
12
|
+
to: string;
|
|
13
|
+
statusCode: number;
|
|
14
|
+
durationMs: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RedirectChain {
|
|
18
|
+
originalUrl: string;
|
|
19
|
+
finalUrl: string;
|
|
20
|
+
hops: RedirectHop[];
|
|
21
|
+
isCircular: boolean;
|
|
22
|
+
hopCount: number;
|
|
23
|
+
totalDurationMs: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
import type { Page } from 'playwright';
|
|
27
|
+
|
|
28
|
+
export async function trackRedirects(page: Page, url: string): Promise<RedirectChain> {
|
|
29
|
+
const hops: RedirectHop[] = [];
|
|
30
|
+
const seen = new Set<string>();
|
|
31
|
+
let lastResponseTime = Date.now();
|
|
32
|
+
|
|
33
|
+
const onResponse = (response: any) => {
|
|
34
|
+
const status = response.status();
|
|
35
|
+
if (status >= 300 && status < 400) {
|
|
36
|
+
const from = response.url();
|
|
37
|
+
const to = response.headers()['location'] ?? '';
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
hops.push({ from, to, statusCode: status, durationMs: now - lastResponseTime });
|
|
40
|
+
lastResponseTime = now;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
page.on('response', onResponse);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 }).catch(() => {});
|
|
48
|
+
} finally {
|
|
49
|
+
page.off('response', onResponse);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const finalUrl = page.url();
|
|
53
|
+
const isCircular = hops.some(h => {
|
|
54
|
+
if (seen.has(h.to)) return true;
|
|
55
|
+
seen.add(h.from);
|
|
56
|
+
return false;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
originalUrl: url,
|
|
61
|
+
finalUrl,
|
|
62
|
+
hops,
|
|
63
|
+
isCircular,
|
|
64
|
+
hopCount: hops.length,
|
|
65
|
+
totalDurationMs: hops.reduce((sum, h) => sum + h.durationMs, 0),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class RedirectRegistry {
|
|
70
|
+
private chains = new Map<string, RedirectChain>();
|
|
71
|
+
|
|
72
|
+
register(chain: RedirectChain): void {
|
|
73
|
+
this.chains.set(chain.originalUrl, chain);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get(url: string): RedirectChain | undefined {
|
|
77
|
+
return this.chains.get(url);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getAll(): RedirectChain[] {
|
|
81
|
+
return [...this.chains.values()];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
getProblematic(): RedirectChain[] {
|
|
85
|
+
return [...this.chains.values()].filter(c => c.isCircular || c.hopCount > 3);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
stats(): { total: number; withRedirects: number; circular: number; deepChains: number } {
|
|
89
|
+
const all = [...this.chains.values()];
|
|
90
|
+
return {
|
|
91
|
+
total: all.length,
|
|
92
|
+
withRedirects: all.filter(c => c.hopCount > 0).length,
|
|
93
|
+
circular: all.filter(c => c.isCircular).length,
|
|
94
|
+
deepChains: all.filter(c => c.hopCount > 3).length,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis-backed distributed crawl frontier.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the in-memory CrawlFrontier for horizontally-scaled crawler deployments.
|
|
5
|
+
* Multiple crawler workers on different containers can share the same URL queue for
|
|
6
|
+
* a given job — enabling 10x crawl throughput on large enterprise apps.
|
|
7
|
+
*
|
|
8
|
+
* Redis key namespace: zeta:frontier:{jobId}
|
|
9
|
+
* LIST zeta:frontier:{jobId}:queue — BFS queue (LPUSH/RPOP)
|
|
10
|
+
* SET zeta:frontier:{jobId}:visited — deduplication set
|
|
11
|
+
* EXPIRE — 24h TTL on all keys
|
|
12
|
+
*
|
|
13
|
+
* Graceful fallback: if REDIS_URL is not set, RedisCrawlFrontier silently falls
|
|
14
|
+
* back to in-memory arrays so local development works without Redis.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const FRONTIER_TTL = 86400; // 24h in seconds
|
|
18
|
+
|
|
19
|
+
export class RedisCrawlFrontier {
|
|
20
|
+
private redis: any;
|
|
21
|
+
private queueKey: string;
|
|
22
|
+
private visitedKey: string;
|
|
23
|
+
private inMemoryQueue: string[] = [];
|
|
24
|
+
private inMemoryVisited = new Set<string>();
|
|
25
|
+
private useRedis: boolean;
|
|
26
|
+
|
|
27
|
+
constructor(private readonly jobId: string) {
|
|
28
|
+
this.queueKey = `zeta:frontier:${jobId}:queue`;
|
|
29
|
+
this.visitedKey = `zeta:frontier:${jobId}:visited`;
|
|
30
|
+
this.useRedis = !!process.env.REDIS_URL;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async init(): Promise<void> {
|
|
34
|
+
if (!this.useRedis) return;
|
|
35
|
+
try {
|
|
36
|
+
const Redis: any = await import('ioredis').then(m => m.default ?? m);
|
|
37
|
+
this.redis = new Redis(process.env.REDIS_URL!, {
|
|
38
|
+
maxRetriesPerRequest: 2,
|
|
39
|
+
enableReadyCheck: false,
|
|
40
|
+
lazyConnect: true,
|
|
41
|
+
});
|
|
42
|
+
await this.redis.connect().catch(() => {
|
|
43
|
+
this.useRedis = false;
|
|
44
|
+
this.redis = null;
|
|
45
|
+
});
|
|
46
|
+
} catch {
|
|
47
|
+
this.useRedis = false;
|
|
48
|
+
this.redis = null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Add a URL to the frontier. No-op if already visited. Returns true if added. */
|
|
53
|
+
async push(url: string): Promise<boolean> {
|
|
54
|
+
if (this.useRedis && this.redis) {
|
|
55
|
+
try {
|
|
56
|
+
const isNew = await this.redis.sadd(this.visitedKey, url);
|
|
57
|
+
if (!isNew) return false;
|
|
58
|
+
await this.redis.lpush(this.queueKey, url);
|
|
59
|
+
await this.redis.expire(this.queueKey, FRONTIER_TTL);
|
|
60
|
+
await this.redis.expire(this.visitedKey, FRONTIER_TTL);
|
|
61
|
+
return true;
|
|
62
|
+
} catch {
|
|
63
|
+
// Fall through to in-memory on Redis error
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (this.inMemoryVisited.has(url)) return false;
|
|
67
|
+
this.inMemoryVisited.add(url);
|
|
68
|
+
this.inMemoryQueue.push(url);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Pop the next URL (FIFO/BFS). Returns null if queue empty. */
|
|
73
|
+
async shift(): Promise<string | null> {
|
|
74
|
+
if (this.useRedis && this.redis) {
|
|
75
|
+
try {
|
|
76
|
+
const url = await this.redis.rpop(this.queueKey);
|
|
77
|
+
return url ?? null;
|
|
78
|
+
} catch {
|
|
79
|
+
// Fall through
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return this.inMemoryQueue.shift() ?? null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Check if a URL has already been visited/queued. */
|
|
86
|
+
async hasVisited(url: string): Promise<boolean> {
|
|
87
|
+
if (this.useRedis && this.redis) {
|
|
88
|
+
try {
|
|
89
|
+
return (await this.redis.sismember(this.visitedKey, url)) === 1;
|
|
90
|
+
} catch { /* fall through */ }
|
|
91
|
+
}
|
|
92
|
+
return this.inMemoryVisited.has(url);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Current queue length. */
|
|
96
|
+
async length(): Promise<number> {
|
|
97
|
+
if (this.useRedis && this.redis) {
|
|
98
|
+
try {
|
|
99
|
+
return await this.redis.llen(this.queueKey);
|
|
100
|
+
} catch { /* fall through */ }
|
|
101
|
+
}
|
|
102
|
+
return this.inMemoryQueue.length;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Total visited count. */
|
|
106
|
+
async visitedCount(): Promise<number> {
|
|
107
|
+
if (this.useRedis && this.redis) {
|
|
108
|
+
try {
|
|
109
|
+
return await this.redis.scard(this.visitedKey);
|
|
110
|
+
} catch { /* fall through */ }
|
|
111
|
+
}
|
|
112
|
+
return this.inMemoryVisited.size;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Seed initial URLs (used at job start). */
|
|
116
|
+
async seed(urls: string[]): Promise<void> {
|
|
117
|
+
for (const url of urls) await this.push(url);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Clean up all Redis keys for this frontier. Call at job completion. */
|
|
121
|
+
async cleanup(): Promise<void> {
|
|
122
|
+
if (this.useRedis && this.redis) {
|
|
123
|
+
try {
|
|
124
|
+
await this.redis.del(this.queueKey, this.visitedKey);
|
|
125
|
+
} catch { /* ignore */ }
|
|
126
|
+
try { await this.redis.quit(); } catch { /* ignore */ }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Stats for monitoring. */
|
|
131
|
+
async stats(): Promise<{ backend: 'redis' | 'memory'; queued: number; visited: number }> {
|
|
132
|
+
return {
|
|
133
|
+
backend: this.useRedis && this.redis ? 'redis' : 'memory',
|
|
134
|
+
queued: await this.length(),
|
|
135
|
+
visited: await this.visitedCount(),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Check if Redis is configured for distributed frontier. */
|
|
141
|
+
export function isRedisFrontierEnabled(): boolean {
|
|
142
|
+
return !!process.env.REDIS_URL;
|
|
143
|
+
}
|