@wenathlan/saddle 1.8.17 → 2.0.2
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 +1339 -0
- package/README.md +68 -22
- package/alternatives.ts +1218 -0
- package/biome.json +153 -0
- package/boards.json +1517 -0
- package/compute.ts +3144 -0
- package/cores.json +1801 -0
- package/dist/acquisition.d.ts +359 -0
- package/dist/acquisition.d.ts.map +1 -0
- package/dist/acquisition.js +629 -0
- package/dist/acquisition.js.map +1 -0
- package/dist/alternatives.d.ts +509 -0
- package/dist/alternatives.d.ts.map +1 -0
- package/dist/alternatives.js +951 -0
- package/dist/alternatives.js.map +1 -0
- package/dist/automation.d.ts +426 -0
- package/dist/automation.d.ts.map +1 -0
- package/dist/automation.js +561 -0
- package/dist/automation.js.map +1 -0
- package/dist/browser.d.ts +562 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +1057 -0
- package/dist/browser.js.map +1 -0
- package/dist/capacitor.config.d.ts +13 -0
- package/dist/capacitor.config.d.ts.map +1 -0
- package/dist/capacitor.config.js +26 -0
- package/dist/capacitor.config.js.map +1 -0
- package/dist/cli.d.ts +17 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +111 -0
- package/dist/cli.js.map +1 -0
- package/dist/communication.d.ts +192 -0
- package/dist/communication.d.ts.map +1 -0
- package/dist/communication.js +418 -0
- package/dist/communication.js.map +1 -0
- package/dist/compute.d.ts +1161 -0
- package/dist/compute.d.ts.map +1 -0
- package/dist/compute.js +2273 -0
- package/dist/compute.js.map +1 -0
- package/dist/distribution.d.ts +529 -0
- package/dist/distribution.d.ts.map +1 -0
- package/dist/distribution.js +811 -0
- package/dist/distribution.js.map +1 -0
- package/dist/execution.d.ts +530 -0
- package/dist/execution.d.ts.map +1 -0
- package/dist/execution.js +761 -0
- package/dist/execution.js.map +1 -0
- package/dist/format.d.ts +19 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +60 -0
- package/dist/format.js.map +1 -0
- package/dist/foundation.d.ts +176 -0
- package/dist/foundation.d.ts.map +1 -0
- package/dist/foundation.js +309 -0
- package/dist/foundation.js.map +1 -0
- package/dist/index.d.ts +733 -122
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1018 -122
- package/dist/index.js.map +1 -1
- package/dist/integration.d.ts +236 -0
- package/dist/integration.d.ts.map +1 -0
- package/dist/integration.js +236 -0
- package/dist/integration.js.map +1 -0
- package/dist/intelligence.d.ts +94 -0
- package/dist/intelligence.d.ts.map +1 -0
- package/dist/intelligence.js +145 -0
- package/dist/intelligence.js.map +1 -0
- package/dist/isolation.d.ts +129 -0
- package/dist/isolation.d.ts.map +1 -0
- package/dist/isolation.js +96 -0
- package/dist/isolation.js.map +1 -0
- package/dist/media.d.ts +344 -0
- package/dist/media.d.ts.map +1 -0
- package/dist/media.js +986 -0
- package/dist/media.js.map +1 -0
- package/dist/modes.d.ts +359 -0
- package/dist/modes.d.ts.map +1 -0
- package/dist/modes.js +134 -0
- package/dist/modes.js.map +1 -0
- package/dist/{library/public.d.ts → operations.d.ts} +15 -1
- package/dist/operations.d.ts.map +1 -0
- package/dist/{library/public.js → operations.js} +18 -7
- package/dist/operations.js.map +1 -0
- package/dist/orchestrator.d.ts +1339 -0
- package/dist/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator.js +4308 -0
- package/dist/orchestrator.js.map +1 -0
- package/dist/performance.d.ts +416 -0
- package/dist/performance.d.ts.map +1 -0
- package/dist/performance.js +827 -0
- package/dist/performance.js.map +1 -0
- package/dist/quantum.d.ts +810 -0
- package/dist/quantum.d.ts.map +1 -0
- package/dist/quantum.js +2073 -0
- package/dist/quantum.js.map +1 -0
- package/dist/render.d.ts +381 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +1073 -0
- package/dist/render.js.map +1 -0
- package/dist/scheduler.d.ts +596 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +1259 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/security.d.ts +501 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +1170 -0
- package/dist/security.js.map +1 -0
- package/dist/server.d.ts +21 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/{server/node.js → server.js} +16 -2
- package/dist/server.js.map +1 -0
- package/dist/tiers.d.ts +1002 -0
- package/dist/tiers.d.ts.map +1 -0
- package/dist/tiers.js +2165 -0
- package/dist/tiers.js.map +1 -0
- package/dist/virtual.d.ts +641 -0
- package/dist/virtual.d.ts.map +1 -0
- package/dist/virtual.js +1285 -0
- package/dist/virtual.js.map +1 -0
- package/dist/virtualcpu.d.ts +316 -0
- package/dist/virtualcpu.d.ts.map +1 -0
- package/dist/virtualcpu.js +1180 -0
- package/dist/virtualcpu.js.map +1 -0
- package/dist/virtualgpu.d.ts +345 -0
- package/dist/virtualgpu.d.ts.map +1 -0
- package/dist/virtualgpu.js +1000 -0
- package/dist/virtualgpu.js.map +1 -0
- package/dist/virtualization.d.ts +607 -0
- package/dist/virtualization.d.ts.map +1 -0
- package/dist/virtualization.js +1187 -0
- package/dist/virtualization.js.map +1 -0
- package/dist/virtualmemory.d.ts +818 -0
- package/dist/virtualmemory.d.ts.map +1 -0
- package/dist/virtualmemory.js +1796 -0
- package/dist/virtualmemory.js.map +1 -0
- package/dist/webscrape.d.ts +720 -0
- package/dist/webscrape.d.ts.map +1 -0
- package/dist/webscrape.js +2697 -0
- package/dist/webscrape.js.map +1 -0
- package/docker.config +769 -0
- package/docs/CONVERSA.txt +1523 -0
- package/docs/alternatives.md +499 -0
- package/docs/architecture-1.8.19.md +76 -0
- package/docs/architecture-1.8.19.mmd +33 -0
- package/docs/architecture-1.8.19.png +0 -0
- package/docs/architecture.md +355 -0
- package/docs/artifactavailability.md +6 -0
- package/docs/brancharchive-2026-08-18.md +18 -0
- package/docs/browser.md +7 -0
- package/docs/consolidation.md +70 -0
- package/docs/e2ugh-engine.md +528 -0
- package/{dist/examples/localjob.js → docs/example-localjob.ts} +3 -3
- package/{dist/examples/publicapi.js → docs/example-publicapi.ts} +2 -2
- package/docs/hardware.md +342 -0
- package/docs/optimization.md +126 -0
- package/docs/performance.md +499 -0
- package/docs/planning.1.8.18.md +94 -0
- package/docs/planning.1.8.19.md +49 -0
- package/docs/releasenotes-1.8.17.md +6 -0
- package/docs/releasenotes-1.8.18.md +32 -0
- package/docs/releasenotes-1.8.19.md +35 -0
- package/docs/research-1.8.18-isolation.md +43 -0
- package/docs/research-1.8.19-virtual-browser.md +27 -0
- package/docs/saddle.archive.1.8.17.tar.gz.gpg +0 -0
- package/docs/security.md +454 -0
- package/docs/todo-1.8.16.md +56 -1
- package/docs/todo-1.8.18.md +164 -0
- package/docs/todo-1.8.19.md +241 -0
- package/docs/viability.md +777 -0
- package/docs/virtualization.md +335 -0
- package/docs/web-duplicatecleanup.md +9 -0
- package/docs/web-ideas.md +106 -0
- package/docs/workflowimprovements-2026-08-19.md +70 -0
- package/gpumonitor.cpp +1543 -0
- package/gpus.json +2408 -0
- package/index.ts +1478 -0
- package/media.ts +1337 -0
- package/mttg.config +1549 -0
- package/orchestrator.ts +5462 -0
- package/package.json +304 -55
- package/passage.config +1331 -0
- package/performance.ts +1049 -0
- package/processors.json +2217 -0
- package/qemu.config +1062 -0
- package/qemubridge.py +1340 -0
- package/quantum.ts +2393 -0
- package/render.ts +1325 -0
- package/scheduler.ts +1600 -0
- package/security.ts +1449 -0
- package/tiers.ts +2871 -0
- package/tsconfig.json +29 -0
- package/virtualcpu.ts +1273 -0
- package/virtualgpu.ts +1181 -0
- package/virtualhardware.c +1011 -0
- package/virtualhardware.json +739 -0
- package/virtualization.ts +1628 -0
- package/virtualizationcore.cpp +4635 -0
- package/virtualmemory.ts +2303 -0
- package/vm.config.json +1910 -0
- package/{extension → web/extension}/manifest.json +1 -1
- package/web/readme.md +382 -0
- package/web/tsconfig.json +25 -0
- package/dist/adapters/forge.d.ts +0 -17
- package/dist/adapters/forge.d.ts.map +0 -1
- package/dist/adapters/forge.js +0 -19
- package/dist/adapters/forge.js.map +0 -1
- package/dist/adapters/forgejo.d.ts +0 -49
- package/dist/adapters/forgejo.d.ts.map +0 -1
- package/dist/adapters/forgejo.js +0 -8
- package/dist/adapters/forgejo.js.map +0 -1
- package/dist/adapters/github.d.ts +0 -12
- package/dist/adapters/github.d.ts.map +0 -1
- package/dist/adapters/github.js +0 -20
- package/dist/adapters/github.js.map +0 -1
- package/dist/adapters/gitlab.d.ts +0 -17
- package/dist/adapters/gitlab.d.ts.map +0 -1
- package/dist/adapters/gitlab.js +0 -10
- package/dist/adapters/gitlab.js.map +0 -1
- package/dist/adapters/huggingface.d.ts +0 -17
- package/dist/adapters/huggingface.d.ts.map +0 -1
- package/dist/adapters/huggingface.js +0 -6
- package/dist/adapters/huggingface.js.map +0 -1
- package/dist/adapters/socket.d.ts +0 -11
- package/dist/adapters/socket.d.ts.map +0 -1
- package/dist/adapters/socket.js +0 -17
- package/dist/adapters/socket.js.map +0 -1
- package/dist/adapters/transport.d.ts +0 -7
- package/dist/adapters/transport.d.ts.map +0 -1
- package/dist/adapters/transport.js +0 -36
- package/dist/adapters/transport.js.map +0 -1
- package/dist/ai/chunk.d.ts +0 -2
- package/dist/ai/chunk.d.ts.map +0 -1
- package/dist/ai/chunk.js +0 -36
- package/dist/ai/chunk.js.map +0 -1
- package/dist/ai/llmstxt.d.ts +0 -6
- package/dist/ai/llmstxt.d.ts.map +0 -1
- package/dist/ai/llmstxt.js +0 -12
- package/dist/ai/llmstxt.js.map +0 -1
- package/dist/ai/provenance.d.ts +0 -22
- package/dist/ai/provenance.d.ts.map +0 -1
- package/dist/ai/provenance.js +0 -25
- package/dist/ai/provenance.js.map +0 -1
- package/dist/ai/rag.d.ts +0 -26
- package/dist/ai/rag.d.ts.map +0 -1
- package/dist/ai/rag.js +0 -22
- package/dist/ai/rag.js.map +0 -1
- package/dist/ai/tokens.d.ts +0 -15
- package/dist/ai/tokens.d.ts.map +0 -1
- package/dist/ai/tokens.js +0 -9
- package/dist/ai/tokens.js.map +0 -1
- package/dist/api/auth.d.ts +0 -16
- package/dist/api/auth.d.ts.map +0 -1
- package/dist/api/auth.js +0 -19
- package/dist/api/auth.js.map +0 -1
- package/dist/api/contracts.d.ts +0 -28
- package/dist/api/contracts.d.ts.map +0 -1
- package/dist/api/contracts.js +0 -14
- package/dist/api/contracts.js.map +0 -1
- package/dist/api/control.d.ts +0 -10
- package/dist/api/control.d.ts.map +0 -1
- package/dist/api/control.js +0 -35
- package/dist/api/control.js.map +0 -1
- package/dist/api/http.d.ts +0 -7
- package/dist/api/http.d.ts.map +0 -1
- package/dist/api/http.js +0 -12
- package/dist/api/http.js.map +0 -1
- package/dist/api/rate.d.ts +0 -28
- package/dist/api/rate.d.ts.map +0 -1
- package/dist/api/rate.js +0 -36
- package/dist/api/rate.js.map +0 -1
- package/dist/api/security.d.ts +0 -12
- package/dist/api/security.d.ts.map +0 -1
- package/dist/api/security.js +0 -56
- package/dist/api/security.js.map +0 -1
- package/dist/api/service.d.ts +0 -5
- package/dist/api/service.d.ts.map +0 -1
- package/dist/api/service.js +0 -70
- package/dist/api/service.js.map +0 -1
- package/dist/apps/registry.d.ts +0 -33
- package/dist/apps/registry.d.ts.map +0 -1
- package/dist/apps/registry.js +0 -24
- package/dist/apps/registry.js.map +0 -1
- package/dist/binary/archive.d.ts +0 -27
- package/dist/binary/archive.d.ts.map +0 -1
- package/dist/binary/archive.js +0 -46
- package/dist/binary/archive.js.map +0 -1
- package/dist/binary/build.d.ts +0 -29
- package/dist/binary/build.d.ts.map +0 -1
- package/dist/binary/build.js +0 -17
- package/dist/binary/build.js.map +0 -1
- package/dist/binary/transform.d.ts +0 -70
- package/dist/binary/transform.d.ts.map +0 -1
- package/dist/binary/transform.js +0 -105
- package/dist/binary/transform.js.map +0 -1
- package/dist/bot/adapter.d.ts +0 -5
- package/dist/bot/adapter.d.ts.map +0 -1
- package/dist/bot/adapter.js +0 -11
- package/dist/bot/adapter.js.map +0 -1
- package/dist/bot/bot.d.ts +0 -28
- package/dist/bot/bot.d.ts.map +0 -1
- package/dist/bot/bot.js +0 -52
- package/dist/bot/bot.js.map +0 -1
- package/dist/bot/commands.d.ts +0 -8
- package/dist/bot/commands.d.ts.map +0 -1
- package/dist/bot/commands.js +0 -19
- package/dist/bot/commands.js.map +0 -1
- package/dist/bot/permissions.d.ts +0 -13
- package/dist/bot/permissions.d.ts.map +0 -1
- package/dist/bot/permissions.js +0 -16
- package/dist/bot/permissions.js.map +0 -1
- package/dist/browser/actions.d.ts +0 -33
- package/dist/browser/actions.d.ts.map +0 -1
- package/dist/browser/actions.js +0 -44
- package/dist/browser/actions.js.map +0 -1
- package/dist/browser/agent.d.ts +0 -21
- package/dist/browser/agent.d.ts.map +0 -1
- package/dist/browser/agent.js +0 -13
- package/dist/browser/agent.js.map +0 -1
- package/dist/browser/context.d.ts +0 -43
- package/dist/browser/context.d.ts.map +0 -1
- package/dist/browser/context.js +0 -54
- package/dist/browser/context.js.map +0 -1
- package/dist/browser/fingerprint.d.ts +0 -4
- package/dist/browser/fingerprint.d.ts.map +0 -1
- package/dist/browser/fingerprint.js +0 -12
- package/dist/browser/fingerprint.js.map +0 -1
- package/dist/browser/index.d.ts +0 -11
- package/dist/browser/index.d.ts.map +0 -1
- package/dist/browser/index.js +0 -11
- package/dist/browser/index.js.map +0 -1
- package/dist/browser/playwright.d.ts +0 -11
- package/dist/browser/playwright.d.ts.map +0 -1
- package/dist/browser/playwright.js +0 -24
- package/dist/browser/playwright.js.map +0 -1
- package/dist/browser/recorder.d.ts +0 -21
- package/dist/browser/recorder.d.ts.map +0 -1
- package/dist/browser/recorder.js +0 -58
- package/dist/browser/recorder.js.map +0 -1
- package/dist/browser/session.d.ts +0 -14
- package/dist/browser/session.d.ts.map +0 -1
- package/dist/browser/session.js +0 -21
- package/dist/browser/session.js.map +0 -1
- package/dist/browser/snapshot.d.ts +0 -69
- package/dist/browser/snapshot.d.ts.map +0 -1
- package/dist/browser/snapshot.js +0 -133
- package/dist/browser/snapshot.js.map +0 -1
- package/dist/captcha/contract.d.ts +0 -35
- package/dist/captcha/contract.d.ts.map +0 -1
- package/dist/captcha/contract.js +0 -17
- package/dist/captcha/contract.js.map +0 -1
- package/dist/captcha/evidence.d.ts +0 -10
- package/dist/captcha/evidence.d.ts.map +0 -1
- package/dist/captcha/evidence.js +0 -9
- package/dist/captcha/evidence.js.map +0 -1
- package/dist/captcha/guard.d.ts +0 -23
- package/dist/captcha/guard.d.ts.map +0 -1
- package/dist/captcha/guard.js +0 -12
- package/dist/captcha/guard.js.map +0 -1
- package/dist/cli/main.d.ts +0 -4
- package/dist/cli/main.d.ts.map +0 -1
- package/dist/cli/main.js +0 -46
- package/dist/cli/main.js.map +0 -1
- package/dist/core/errors.d.ts +0 -84
- package/dist/core/errors.d.ts.map +0 -1
- package/dist/core/errors.js +0 -54
- package/dist/core/errors.js.map +0 -1
- package/dist/core/events.d.ts +0 -9
- package/dist/core/events.d.ts.map +0 -1
- package/dist/core/events.js +0 -21
- package/dist/core/events.js.map +0 -1
- package/dist/core/hash.d.ts +0 -10
- package/dist/core/hash.d.ts.map +0 -1
- package/dist/core/hash.js +0 -82
- package/dist/core/hash.js.map +0 -1
- package/dist/core/ids.d.ts +0 -10
- package/dist/core/ids.d.ts.map +0 -1
- package/dist/core/ids.js +0 -15
- package/dist/core/ids.js.map +0 -1
- package/dist/delivery/manifest.d.ts +0 -35
- package/dist/delivery/manifest.d.ts.map +0 -1
- package/dist/delivery/manifest.js +0 -68
- package/dist/delivery/manifest.js.map +0 -1
- package/dist/deploy/index.d.ts +0 -6
- package/dist/deploy/index.d.ts.map +0 -1
- package/dist/deploy/index.js +0 -6
- package/dist/deploy/index.js.map +0 -1
- package/dist/dispatch/resumable.d.ts +0 -102
- package/dist/dispatch/resumable.d.ts.map +0 -1
- package/dist/dispatch/resumable.js +0 -64
- package/dist/dispatch/resumable.js.map +0 -1
- package/dist/dispatch/workflow.d.ts +0 -6
- package/dist/dispatch/workflow.d.ts.map +0 -1
- package/dist/dispatch/workflow.js +0 -37
- package/dist/dispatch/workflow.js.map +0 -1
- package/dist/domain/artifacts.d.ts +0 -12
- package/dist/domain/artifacts.d.ts.map +0 -1
- package/dist/domain/artifacts.js +0 -14
- package/dist/domain/artifacts.js.map +0 -1
- package/dist/domain/jobs.d.ts +0 -12
- package/dist/domain/jobs.d.ts.map +0 -1
- package/dist/domain/jobs.js +0 -20
- package/dist/domain/jobs.js.map +0 -1
- package/dist/domain/providers.d.ts +0 -10
- package/dist/domain/providers.d.ts.map +0 -1
- package/dist/domain/providers.js +0 -8
- package/dist/domain/providers.js.map +0 -1
- package/dist/domain/runtime.d.ts +0 -14
- package/dist/domain/runtime.d.ts.map +0 -1
- package/dist/domain/runtime.js +0 -10
- package/dist/domain/runtime.js.map +0 -1
- package/dist/domain/sessions.d.ts +0 -12
- package/dist/domain/sessions.d.ts.map +0 -1
- package/dist/domain/sessions.js +0 -57
- package/dist/domain/sessions.js.map +0 -1
- package/dist/examples/localjob.d.ts +0 -2
- package/dist/examples/localjob.d.ts.map +0 -1
- package/dist/examples/localjob.js.map +0 -1
- package/dist/examples/publicapi.d.ts +0 -2
- package/dist/examples/publicapi.d.ts.map +0 -1
- package/dist/examples/publicapi.js.map +0 -1
- package/dist/extension/build.d.ts +0 -9
- package/dist/extension/build.d.ts.map +0 -1
- package/dist/extension/build.js +0 -85
- package/dist/extension/build.js.map +0 -1
- package/dist/extension/content.d.ts +0 -6
- package/dist/extension/content.d.ts.map +0 -1
- package/dist/extension/content.js +0 -135
- package/dist/extension/content.js.map +0 -1
- package/dist/extension/index.d.ts +0 -7
- package/dist/extension/index.d.ts.map +0 -1
- package/dist/extension/index.js +0 -7
- package/dist/extension/index.js.map +0 -1
- package/dist/extension/pagebridge.d.ts +0 -6
- package/dist/extension/pagebridge.d.ts.map +0 -1
- package/dist/extension/pagebridge.js +0 -42
- package/dist/extension/pagebridge.js.map +0 -1
- package/dist/extension/permissions.d.ts +0 -26
- package/dist/extension/permissions.d.ts.map +0 -1
- package/dist/extension/permissions.js +0 -28
- package/dist/extension/permissions.js.map +0 -1
- package/dist/extension/popup.d.ts +0 -5
- package/dist/extension/popup.d.ts.map +0 -1
- package/dist/extension/popup.js +0 -26
- package/dist/extension/popup.js.map +0 -1
- package/dist/extension/protocol.d.ts +0 -58
- package/dist/extension/protocol.d.ts.map +0 -1
- package/dist/extension/protocol.js +0 -101
- package/dist/extension/protocol.js.map +0 -1
- package/dist/extension/serviceworker.d.ts +0 -25
- package/dist/extension/serviceworker.d.ts.map +0 -1
- package/dist/extension/serviceworker.js +0 -102
- package/dist/extension/serviceworker.js.map +0 -1
- package/dist/extension/worker.d.ts +0 -28
- package/dist/extension/worker.d.ts.map +0 -1
- package/dist/extension/worker.js +0 -22
- package/dist/extension/worker.js.map +0 -1
- package/dist/format/check.d.ts +0 -8
- package/dist/format/check.d.ts.map +0 -1
- package/dist/format/check.js +0 -38
- package/dist/format/check.js.map +0 -1
- package/dist/library/public.d.ts.map +0 -1
- package/dist/library/public.js.map +0 -1
- package/dist/mcp/browser.d.ts +0 -9
- package/dist/mcp/browser.d.ts.map +0 -1
- package/dist/mcp/browser.js +0 -13
- package/dist/mcp/browser.js.map +0 -1
- package/dist/mcp/server.d.ts +0 -35
- package/dist/mcp/server.d.ts.map +0 -1
- package/dist/mcp/server.js +0 -40
- package/dist/mcp/server.js.map +0 -1
- package/dist/mcp/transport.d.ts +0 -7
- package/dist/mcp/transport.d.ts.map +0 -1
- package/dist/mcp/transport.js +0 -17
- package/dist/mcp/transport.js.map +0 -1
- package/dist/memory/bridge.d.ts +0 -14
- package/dist/memory/bridge.d.ts.map +0 -1
- package/dist/memory/bridge.js +0 -16
- package/dist/memory/bridge.js.map +0 -1
- package/dist/memory/engine.d.ts +0 -38
- package/dist/memory/engine.d.ts.map +0 -1
- package/dist/memory/engine.js +0 -109
- package/dist/memory/engine.js.map +0 -1
- package/dist/memory/modes.d.ts +0 -43
- package/dist/memory/modes.d.ts.map +0 -1
- package/dist/memory/modes.js +0 -66
- package/dist/memory/modes.js.map +0 -1
- package/dist/memory/objects.d.ts +0 -21
- package/dist/memory/objects.d.ts.map +0 -1
- package/dist/memory/objects.js +0 -20
- package/dist/memory/objects.js.map +0 -1
- package/dist/memory/planner.d.ts +0 -66
- package/dist/memory/planner.d.ts.map +0 -1
- package/dist/memory/planner.js +0 -108
- package/dist/memory/planner.js.map +0 -1
- package/dist/memory/targets.d.ts +0 -78
- package/dist/memory/targets.d.ts.map +0 -1
- package/dist/memory/targets.js +0 -29
- package/dist/memory/targets.js.map +0 -1
- package/dist/memory/transforms.d.ts +0 -19
- package/dist/memory/transforms.d.ts.map +0 -1
- package/dist/memory/transforms.js +0 -14
- package/dist/memory/transforms.js.map +0 -1
- package/dist/modes/matrix.d.ts +0 -23
- package/dist/modes/matrix.d.ts.map +0 -1
- package/dist/modes/matrix.js +0 -18
- package/dist/modes/matrix.js.map +0 -1
- package/dist/modes/modes.d.ts +0 -33
- package/dist/modes/modes.d.ts.map +0 -1
- package/dist/modes/modes.js +0 -15
- package/dist/modes/modes.js.map +0 -1
- package/dist/modes/resolve.d.ts +0 -96
- package/dist/modes/resolve.d.ts.map +0 -1
- package/dist/modes/resolve.js +0 -48
- package/dist/modes/resolve.js.map +0 -1
- package/dist/observability/metrics.d.ts +0 -14
- package/dist/observability/metrics.d.ts.map +0 -1
- package/dist/observability/metrics.js +0 -18
- package/dist/observability/metrics.js.map +0 -1
- package/dist/packager/manifest.d.ts +0 -66
- package/dist/packager/manifest.d.ts.map +0 -1
- package/dist/packager/manifest.js +0 -94
- package/dist/packager/manifest.js.map +0 -1
- package/dist/packager/publish.d.ts +0 -51
- package/dist/packager/publish.d.ts.map +0 -1
- package/dist/packager/publish.js +0 -18
- package/dist/packager/publish.js.map +0 -1
- package/dist/packager/targetcli.d.ts +0 -7
- package/dist/packager/targetcli.d.ts.map +0 -1
- package/dist/packager/targetcli.js +0 -22
- package/dist/packager/targetcli.js.map +0 -1
- package/dist/persistence/adapter.d.ts +0 -5
- package/dist/persistence/adapter.d.ts.map +0 -1
- package/dist/persistence/adapter.js +0 -11
- package/dist/persistence/adapter.js.map +0 -1
- package/dist/persistence/drizzle.d.ts +0 -2
- package/dist/persistence/drizzle.d.ts.map +0 -1
- package/dist/persistence/drizzle.js +0 -12
- package/dist/persistence/drizzle.js.map +0 -1
- package/dist/persistence/memory.d.ts +0 -2
- package/dist/persistence/memory.d.ts.map +0 -1
- package/dist/persistence/memory.js +0 -27
- package/dist/persistence/memory.js.map +0 -1
- package/dist/persistence/migrations.d.ts +0 -19
- package/dist/persistence/migrations.d.ts.map +0 -1
- package/dist/persistence/migrations.js +0 -13
- package/dist/persistence/migrations.js.map +0 -1
- package/dist/persistence/prisma.d.ts +0 -2
- package/dist/persistence/prisma.d.ts.map +0 -1
- package/dist/persistence/prisma.js +0 -25
- package/dist/persistence/prisma.js.map +0 -1
- package/dist/persistence/schema.d.ts +0 -63
- package/dist/persistence/schema.d.ts.map +0 -1
- package/dist/persistence/schema.js +0 -28
- package/dist/persistence/schema.js.map +0 -1
- package/dist/persistence/sql.d.ts +0 -3
- package/dist/persistence/sql.d.ts.map +0 -1
- package/dist/persistence/sql.js +0 -37
- package/dist/persistence/sql.js.map +0 -1
- package/dist/protocol/blocks.d.ts +0 -10
- package/dist/protocol/blocks.d.ts.map +0 -1
- package/dist/protocol/blocks.js +0 -26
- package/dist/protocol/blocks.js.map +0 -1
- package/dist/protocol/json.d.ts +0 -6
- package/dist/protocol/json.d.ts.map +0 -1
- package/dist/protocol/json.js +0 -6
- package/dist/protocol/json.js.map +0 -1
- package/dist/protocol/ndjson.d.ts +0 -3
- package/dist/protocol/ndjson.d.ts.map +0 -1
- package/dist/protocol/ndjson.js +0 -20
- package/dist/protocol/ndjson.js.map +0 -1
- package/dist/protocol/sse.d.ts +0 -7
- package/dist/protocol/sse.d.ts.map +0 -1
- package/dist/protocol/sse.js +0 -25
- package/dist/protocol/sse.js.map +0 -1
- package/dist/proxy/pool.d.ts +0 -9
- package/dist/proxy/pool.d.ts.map +0 -1
- package/dist/proxy/pool.js +0 -27
- package/dist/proxy/pool.js.map +0 -1
- package/dist/queue/idempotency.d.ts +0 -10
- package/dist/queue/idempotency.d.ts.map +0 -1
- package/dist/queue/idempotency.js +0 -13
- package/dist/queue/idempotency.js.map +0 -1
- package/dist/queue/persistent.d.ts +0 -14
- package/dist/queue/persistent.d.ts.map +0 -1
- package/dist/queue/persistent.js +0 -66
- package/dist/queue/persistent.js.map +0 -1
- package/dist/queue/queue.d.ts +0 -7
- package/dist/queue/queue.d.ts.map +0 -1
- package/dist/queue/queue.js +0 -55
- package/dist/queue/queue.js.map +0 -1
- package/dist/queue/saga.d.ts +0 -5
- package/dist/queue/saga.d.ts.map +0 -1
- package/dist/queue/saga.js +0 -20
- package/dist/queue/saga.js.map +0 -1
- package/dist/release/assets.d.ts +0 -144
- package/dist/release/assets.d.ts.map +0 -1
- package/dist/release/assets.js +0 -157
- package/dist/release/assets.js.map +0 -1
- package/dist/release/evidence.d.ts +0 -92
- package/dist/release/evidence.d.ts.map +0 -1
- package/dist/release/evidence.js +0 -148
- package/dist/release/evidence.js.map +0 -1
- package/dist/release/verify.d.ts +0 -12
- package/dist/release/verify.d.ts.map +0 -1
- package/dist/release/verify.js +0 -112
- package/dist/release/verify.js.map +0 -1
- package/dist/runners/chain.d.ts +0 -125
- package/dist/runners/chain.d.ts.map +0 -1
- package/dist/runners/chain.js +0 -95
- package/dist/runners/chain.js.map +0 -1
- package/dist/runners/health.d.ts +0 -36
- package/dist/runners/health.d.ts.map +0 -1
- package/dist/runners/health.js +0 -26
- package/dist/runners/health.js.map +0 -1
- package/dist/runners/heartbeat.d.ts +0 -23
- package/dist/runners/heartbeat.d.ts.map +0 -1
- package/dist/runners/heartbeat.js +0 -29
- package/dist/runners/heartbeat.js.map +0 -1
- package/dist/runners/inprocess.d.ts +0 -17
- package/dist/runners/inprocess.d.ts.map +0 -1
- package/dist/runners/inprocess.js +0 -20
- package/dist/runners/inprocess.js.map +0 -1
- package/dist/runners/scheduler.d.ts +0 -5
- package/dist/runners/scheduler.d.ts.map +0 -1
- package/dist/runners/scheduler.js +0 -19
- package/dist/runners/scheduler.js.map +0 -1
- package/dist/runtime/abort.d.ts +0 -8
- package/dist/runtime/abort.d.ts.map +0 -1
- package/dist/runtime/abort.js +0 -13
- package/dist/runtime/abort.js.map +0 -1
- package/dist/runtime/compatibility.d.ts +0 -37
- package/dist/runtime/compatibility.d.ts.map +0 -1
- package/dist/runtime/compatibility.js +0 -10
- package/dist/runtime/compatibility.js.map +0 -1
- package/dist/runtime/detect.d.ts +0 -13
- package/dist/runtime/detect.d.ts.map +0 -1
- package/dist/runtime/detect.js +0 -18
- package/dist/runtime/detect.js.map +0 -1
- package/dist/runtime/engine.d.ts +0 -20
- package/dist/runtime/engine.d.ts.map +0 -1
- package/dist/runtime/engine.js +0 -61
- package/dist/runtime/engine.js.map +0 -1
- package/dist/runtime/retry.d.ts +0 -19
- package/dist/runtime/retry.d.ts.map +0 -1
- package/dist/runtime/retry.js +0 -54
- package/dist/runtime/retry.js.map +0 -1
- package/dist/runtime/worker.d.ts +0 -10
- package/dist/runtime/worker.d.ts.map +0 -1
- package/dist/runtime/worker.js +0 -25
- package/dist/runtime/worker.js.map +0 -1
- package/dist/scrape/cache.d.ts +0 -45
- package/dist/scrape/cache.d.ts.map +0 -1
- package/dist/scrape/cache.js +0 -45
- package/dist/scrape/cache.js.map +0 -1
- package/dist/scrape/crawl.d.ts +0 -47
- package/dist/scrape/crawl.d.ts.map +0 -1
- package/dist/scrape/crawl.js +0 -114
- package/dist/scrape/crawl.js.map +0 -1
- package/dist/scrape/extract.d.ts +0 -63
- package/dist/scrape/extract.d.ts.map +0 -1
- package/dist/scrape/extract.js +0 -47
- package/dist/scrape/extract.js.map +0 -1
- package/dist/scrape/normalize.d.ts +0 -52
- package/dist/scrape/normalize.d.ts.map +0 -1
- package/dist/scrape/normalize.js +0 -102
- package/dist/scrape/normalize.js.map +0 -1
- package/dist/scrape/robots.d.ts +0 -37
- package/dist/scrape/robots.d.ts.map +0 -1
- package/dist/scrape/robots.js +0 -71
- package/dist/scrape/robots.js.map +0 -1
- package/dist/scrape/schema.d.ts +0 -23
- package/dist/scrape/schema.d.ts.map +0 -1
- package/dist/scrape/schema.js +0 -92
- package/dist/scrape/schema.js.map +0 -1
- package/dist/scrape/scraper.d.ts +0 -6
- package/dist/scrape/scraper.d.ts.map +0 -1
- package/dist/scrape/scraper.js +0 -46
- package/dist/scrape/scraper.js.map +0 -1
- package/dist/scrape/semantic.d.ts +0 -25
- package/dist/scrape/semantic.d.ts.map +0 -1
- package/dist/scrape/semantic.js +0 -27
- package/dist/scrape/semantic.js.map +0 -1
- package/dist/server/node.d.ts +0 -7
- package/dist/server/node.d.ts.map +0 -1
- package/dist/server/node.js.map +0 -1
- package/dist/sessions/file.d.ts +0 -25
- package/dist/sessions/file.d.ts.map +0 -1
- package/dist/sessions/file.js +0 -13
- package/dist/sessions/file.js.map +0 -1
- package/dist/sessions/replay.d.ts +0 -9
- package/dist/sessions/replay.d.ts.map +0 -1
- package/dist/sessions/replay.js +0 -70
- package/dist/sessions/replay.js.map +0 -1
- package/dist/sessions/store.d.ts +0 -25
- package/dist/sessions/store.d.ts.map +0 -1
- package/dist/sessions/store.js +0 -13
- package/dist/sessions/store.js.map +0 -1
- package/dist/storage/adapter.d.ts +0 -5
- package/dist/storage/adapter.d.ts.map +0 -1
- package/dist/storage/adapter.js +0 -11
- package/dist/storage/adapter.js.map +0 -1
- package/dist/storage/cache.d.ts +0 -23
- package/dist/storage/cache.d.ts.map +0 -1
- package/dist/storage/cache.js +0 -79
- package/dist/storage/cache.js.map +0 -1
- package/dist/storage/checksum.d.ts +0 -3
- package/dist/storage/checksum.d.ts.map +0 -1
- package/dist/storage/checksum.js +0 -19
- package/dist/storage/checksum.js.map +0 -1
- package/dist/storage/chunked.d.ts +0 -21
- package/dist/storage/chunked.d.ts.map +0 -1
- package/dist/storage/chunked.js +0 -67
- package/dist/storage/chunked.js.map +0 -1
- package/dist/storage/content.d.ts +0 -24
- package/dist/storage/content.d.ts.map +0 -1
- package/dist/storage/content.js +0 -44
- package/dist/storage/content.js.map +0 -1
- package/dist/storage/filehosting.d.ts +0 -2
- package/dist/storage/filehosting.d.ts.map +0 -1
- package/dist/storage/filehosting.js +0 -24
- package/dist/storage/filehosting.js.map +0 -1
- package/dist/storage/githubcontents.d.ts +0 -2
- package/dist/storage/githubcontents.d.ts.map +0 -1
- package/dist/storage/githubcontents.js +0 -29
- package/dist/storage/githubcontents.js.map +0 -1
- package/dist/storage/index.d.ts +0 -12
- package/dist/storage/index.d.ts.map +0 -1
- package/dist/storage/index.js +0 -12
- package/dist/storage/index.js.map +0 -1
- package/dist/storage/local.d.ts +0 -2
- package/dist/storage/local.d.ts.map +0 -1
- package/dist/storage/local.js +0 -72
- package/dist/storage/local.js.map +0 -1
- package/dist/storage/memory.d.ts +0 -6
- package/dist/storage/memory.d.ts.map +0 -1
- package/dist/storage/memory.js +0 -36
- package/dist/storage/memory.js.map +0 -1
- package/dist/storage/pool.d.ts +0 -95
- package/dist/storage/pool.d.ts.map +0 -1
- package/dist/storage/pool.js +0 -202
- package/dist/storage/pool.js.map +0 -1
- package/dist/storage/s3compatible.d.ts +0 -3
- package/dist/storage/s3compatible.d.ts.map +0 -1
- package/dist/storage/s3compatible.js +0 -70
- package/dist/storage/s3compatible.js.map +0 -1
- package/dist/storage/sync.d.ts +0 -47
- package/dist/storage/sync.d.ts.map +0 -1
- package/dist/storage/sync.js +0 -75
- package/dist/storage/sync.js.map +0 -1
- package/dist/surfaces/adapters.d.ts +0 -97
- package/dist/surfaces/adapters.d.ts.map +0 -1
- package/dist/surfaces/adapters.js +0 -50
- package/dist/surfaces/adapters.js.map +0 -1
- package/dist/surfaces/controls.d.ts +0 -18
- package/dist/surfaces/controls.d.ts.map +0 -1
- package/dist/surfaces/controls.js +0 -45
- package/dist/surfaces/controls.js.map +0 -1
- package/dist/surfaces/manifest.d.ts +0 -68
- package/dist/surfaces/manifest.d.ts.map +0 -1
- package/dist/surfaces/manifest.js +0 -42
- package/dist/surfaces/manifest.js.map +0 -1
- package/dist/surfaces/n8n.d.ts +0 -25
- package/dist/surfaces/n8n.d.ts.map +0 -1
- package/dist/surfaces/n8n.js +0 -31
- package/dist/surfaces/n8n.js.map +0 -1
- package/dist/surfaces/operations.d.ts +0 -32
- package/dist/surfaces/operations.d.ts.map +0 -1
- package/dist/surfaces/operations.js +0 -53
- package/dist/surfaces/operations.js.map +0 -1
- package/dist/surfaces/requirements.d.ts +0 -29
- package/dist/surfaces/requirements.d.ts.map +0 -1
- package/dist/surfaces/requirements.js +0 -50
- package/dist/surfaces/requirements.js.map +0 -1
- package/dist/surfaces/targets.d.ts +0 -196
- package/dist/surfaces/targets.d.ts.map +0 -1
- package/dist/surfaces/targets.js +0 -27
- package/dist/surfaces/targets.js.map +0 -1
- package/dist/webhook/delivery.d.ts +0 -18
- package/dist/webhook/delivery.d.ts.map +0 -1
- package/dist/webhook/delivery.js +0 -36
- package/dist/webhook/delivery.js.map +0 -1
- package/dist/webhook/receiver.d.ts +0 -26
- package/dist/webhook/receiver.d.ts.map +0 -1
- package/dist/webhook/receiver.js +0 -24
- package/dist/webhook/receiver.js.map +0 -1
- package/dist/webhook/signature.d.ts +0 -3
- package/dist/webhook/signature.d.ts.map +0 -1
- package/dist/webhook/signature.js +0 -7
- package/dist/webhook/signature.js.map +0 -1
- package/dist/workflow/manifest.d.ts +0 -22
- package/dist/workflow/manifest.d.ts.map +0 -1
- package/dist/workflow/manifest.js +0 -21
- package/dist/workflow/manifest.js.map +0 -1
- package/dist/workflow/registry.d.ts +0 -7
- package/dist/workflow/registry.d.ts.map +0 -1
- package/dist/workflow/registry.js +0 -18
- package/dist/workflow/registry.js.map +0 -1
- package/dist/workflow/templates.d.ts +0 -6
- package/dist/workflow/templates.d.ts.map +0 -1
- package/dist/workflow/templates.js +0 -18
- package/dist/workflow/templates.js.map +0 -1
- package/dist/workflow/triggers.d.ts +0 -64
- package/dist/workflow/triggers.d.ts.map +0 -1
- package/dist/workflow/triggers.js +0 -99
- package/dist/workflow/triggers.js.map +0 -1
- package/docs/logs/.gitkeep +0 -0
- package/docs/plans/00.index.md +0 -50
- package/docs/plans/01.architecture.md +0 -86
- package/docs/plans/02.research.computer.use.md +0 -58
- package/docs/plans/03.research.captcha.bypass.md +0 -68
- package/docs/plans/04.research.sandbox.ai.md +0 -52
- package/docs/plans/05.capture.platform.md +0 -57
- package/docs/plans/06.dependencies.md +0 -97
- package/docs/plans/07.captcha.test.page.md +0 -41
- package/docs/plans/08.production.infra.md +0 -70
- package/docs/plans/09.database.schema.md +0 -121
- package/docs/plans/10.cloudinary.storage.md +0 -57
- package/docs/plans/11.movement.logs.json.md +0 -72
- package/docs/plans/12.research.atlas.agent.browser.md +0 -79
- package/docs/plans/13.research.anti.detection.md +0 -898
- package/docs/plans/14.research.proxy.md +0 -1495
- package/docs/plans/15.research.retry.rate.limit.md +0 -1958
- package/docs/plans/16.research.crawling.md +0 -1417
- package/docs/plans/17.research.caching.md +0 -1610
- package/docs/plans/18.research.content.extraction.md +0 -1952
- package/docs/plans/19.research.errors.events.md +0 -1523
- package/docs/plans/20.research.zod.validation.md +0 -1350
- package/docs/plans/21.research.batch.concurrency.md +0 -1888
- package/docs/plans/22.research.universal.runtime.md +0 -944
- package/docs/plans/23.research.ai.integration.md +0 -1465
- package/docs/plans/24.research.memory.persistence.md +0 -1979
- package/docs/plans/25.research.server.api.md +0 -342
- package/docs/plans/26.research.compilation.md +0 -249
- package/docs/plans/27.research.html.parsing.md +0 -251
- package/docs/plans/28.action.plan.md +0 -50
- package/docs/plans/29.api.reference.md +0 -174
- package/docs/plans/30.architecture.plan.md +0 -94
- package/docs/plans/31.auditoria.dados.md +0 -163
- package/docs/plans/32.bots.automacao.computacional.md +0 -214
- package/docs/plans/33.bots.codigo.revisao.md +0 -220
- package/docs/plans/34.bots.seguranca.cicd.md +0 -366
- package/docs/plans/35.comparativo.concorrencia.md +0 -464
- package/docs/plans/36.computational.memory.md +0 -340
- package/docs/plans/37.deploystrategy.md +0 -394
- package/docs/plans/38.flow.md +0 -155
- package/docs/plans/39.multi.platform.bot.md +0 -252
- package/docs/plans/40.npm.publish.md +0 -250
- package/docs/plans/41.o.que.falta.md +0 -407
- package/docs/plans/42.pesquisa.concorrencia.md +0 -721
- package/docs/plans/43.plan.universal.architecture.md +0 -496
- package/docs/plans/44.reference.md +0 -100
- package/docs/plans/45.robotarchitecture.md +0 -237
- package/docs/plans/46.scdnintegration.md +0 -284
- package/docs/plans/47.multiforge.readme.md +0 -129
- package/docs/plans/48.theory.v4.repo.os.md +0 -152
- package/docs/plans/49.third.party.infra.md +0 -12
- package/docs/plans/50.file.as.compute.md +0 -39
- package/docs/plans/51.architecture.virtual.processor.md +0 -80
- package/docs/plans/52.manifesto.v8.md +0 -11
- package/docs/plans/58.cdn.list.md +0 -23
- package/docs/plans/59.sql.frameworks.md +0 -33
- package/docs/plans/60.sql.thirdparty.md +0 -26
- package/docs/plans/61.objective.multiforge.md +0 -63
- package/docs/plans/62.huggingface.upload.md +0 -26
- package/docs/plans/63.kaggle.upload.md +0 -24
- package/docs/plans/64.npm.storage.md +0 -30
- package/docs/plans/65.rclone.terabox.md +0 -32
- package/docs/plans/66.buckets.and.models.todo.md +0 -14
- package/docs/plans/67.database.todo.md +0 -13
- package/docs/plans/68.deploy.packages.todo.md +0 -12
- package/docs/plans/69.report.human.operator.md +0 -133
- package/docs/plans/70.report.brain2qwerty.ems.md +0 -135
- package/docs/plans/71.report.hd.infinito.vram.md +0 -155
- package/docs/plans/72.plan.hd.infinito.node.md +0 -146
- package/docs/plans/73.plan.scifi.repos.md +0 -125
- package/docs/plans/74.000.manifesto.v8.flat.2..md +0 -11
- package/docs/plans/README.md +0 -489
- package/docs/plans/aggregate_platforms.mjs +0 -146
- package/docs/plans/examplesession.json +0 -36
- package/docs/plans/missing-facts.md +0 -192
- package/docs/plans/models.md +0 -64
- package/docs/plans/organize.cjs +0 -270
- package/docs/plans/platforms.md +0 -2887
- package/docs/plans/sites.md +0 -31322
- package/docs/sources/farm.py +0 -117
- package/docs/sources/html/saddle1.html +0 -132
- package/docs/sources/html/saddle2.html +0 -157
- package/docs/sources/html/saddle3.html +0 -119
- package/docs/sources/html/saddle4.html +0 -144
- package/docs/sources/html/saddle5.html +0 -72
- package/docs/sources/html/saddle6.html +0 -171
- package/docs/sources/html/saddle7.html +0 -236
- package/docs/sources/saddle.ts +0 -74
- package/docs/sources/schema.prisma +0 -88
- package/docs/sources/script.sh +0 -64
- package/docs/sources/workflows.yml +0 -458
- package/docs/talks1/_body.txt +0 -14
- package/docs/talks1/_index.md +0 -15
- package/docs/talks1/_screenshot.png +0 -0
- package/docs/talks1/assistant-01.md +0 -5
- package/docs/talks1/assistant-02.md +0 -5
- package/docs/talks1/assistant-03.md +0 -531
- package/docs/talks1/assistant-04.md +0 -26
- package/docs/talks1/assistant-05.md +0 -774
- package/docs/talks1/assistant-06.md +0 -1718
- package/docs/talks1/scrape-share.cjs +0 -185
- package/docs/talks1/scrape-share.ts +0 -183
- package/docs/talks1/user-01.md +0 -3
- package/docs/talks1/user-02.md +0 -3
- package/docs/talks1/user-03.md +0 -88
- package/docs/talks1/user-04.md +0 -3
- package/docs/talks1/user-05.md +0 -3
- package/docs/talks1/user-06.md +0 -88
- package/docs/talks1/user-07.md +0 -88
- package/docs/talks2/_body.txt +0 -14
- package/docs/talks2/_index.md +0 -16
- package/docs/talks2/_screenshot.png +0 -0
- package/docs/talks2/assistant-01.md +0 -5
- package/docs/talks2/assistant-02.md +0 -5
- package/docs/talks2/assistant-03.md +0 -424
- package/docs/talks2/assistant-04.md +0 -598
- package/docs/talks2/assistant-05.md +0 -1280
- package/docs/talks2/assistant-06.md +0 -1227
- package/docs/talks2/assistant-07.md +0 -1252
- package/docs/talks2/user-01.md +0 -3
- package/docs/talks2/user-02.md +0 -3
- package/docs/talks2/user-03.md +0 -88
- package/docs/talks2/user-04.md +0 -88
- package/docs/talks2/user-05.md +0 -88
- package/docs/talks2/user-06.md +0 -88
- package/docs/talks2/user-07.md +0 -3
- package/docs/talks3/_body.txt +0 -467
- package/docs/talks3/_index.md +0 -10
- package/docs/talks3/_screenshot.png +0 -0
- package/docs/talks3/assistant-01.md +0 -417
- package/docs/talks3/assistant-02.md +0 -417
- package/docs/talks3/assistant-03.md +0 -29
- package/docs/talks3/assistant-04.md +0 -727
- package/docs/talks3/user-01.md +0 -88
- package/docs/talks3/user-02.md +0 -88
- package/docs/talks3/user-03.md +0 -3
- package/docs/talks3/user-04.md +0 -3
- package/docs/talks4/_body.txt +0 -14
- package/docs/talks4/_index.md +0 -12
- package/docs/talks4/_screenshot.png +0 -0
- package/docs/talks4/assistant-01.md +0 -5
- package/docs/talks4/assistant-02.md +0 -5
- package/docs/talks4/assistant-03.md +0 -35
- package/docs/talks4/assistant-04.md +0 -512
- package/docs/talks4/assistant-05.md +0 -599
- package/docs/talks4/user-01.md +0 -3
- package/docs/talks4/user-02.md +0 -3
- package/docs/talks4/user-03.md +0 -88
- package/docs/talks4/user-04.md +0 -88
- package/docs/talks4/user-05.md +0 -7
- package/docs/talks5/_body.txt +0 -14
- package/docs/talks5/_index.md +0 -13
- package/docs/talks5/_screenshot.png +0 -0
- package/docs/talks5/assistant-01.md +0 -5
- package/docs/talks5/assistant-02.md +0 -5
- package/docs/talks5/assistant-03.md +0 -690
- package/docs/talks5/assistant-04.md +0 -758
- package/docs/talks5/assistant-05.md +0 -974
- package/docs/talks5/user-01.md +0 -3
- package/docs/talks5/user-02.md +0 -3
- package/docs/talks5/user-03.md +0 -105
- package/docs/talks5/user-04.md +0 -105
- package/docs/talks5/user-05.md +0 -63
- package/docs/talks5/user-06.md +0 -105
- package/docs/talks6/_body.txt +0 -14
- package/docs/talks6/_index.md +0 -9
- package/docs/talks6/_screenshot.png +0 -0
- package/docs/talks6/assistant-01.md +0 -5
- package/docs/talks6/assistant-02.md +0 -5
- package/docs/talks6/assistant-03.md +0 -1499
- package/docs/talks6/user-01.md +0 -3
- package/docs/talks6/user-02.md +0 -3
- package/docs/talks6/user-03.md +0 -88
- package/docs/talks6/user-04.md +0 -88
- package/docs/talks7/_body.txt +0 -14
- package/docs/talks7/_index.md +0 -10
- package/docs/talks7/_screenshot.png +0 -0
- package/docs/talks7/assistant-01.md +0 -5
- package/docs/talks7/assistant-02.md +0 -5
- package/docs/talks7/assistant-03.md +0 -523
- package/docs/talks7/assistant-04.md +0 -617
- package/docs/talks7/user-01.md +0 -3
- package/docs/talks7/user-02.md +0 -3
- package/docs/talks7/user-03.md +0 -105
- package/docs/talks7/user-04.md +0 -67
- package/docs/talks8/conversa1.txt +0 -1322
- package/docs/talks8/conversa2.txt +0 -237
- package/docs/talks9/Beyond the Obvious_ 50 Plataformas Auto-Hospedadas de Forja de C/303/263digo para Al/303/251m de Gitea e GitLab.md" +0 -174
- package/docs/talks9/De NPM a Multi-Linguagem_ Uma Arquitetura T/303/251cnica para a Execu/303/247/303/243o Integrada de C/303/263digo no Ecossistema Node.js.md" +0 -59
- package/docs/talks9/De NPM a VMs Virtuais_ Uma An/303/241lise Arquitet/303/264nica para a Realiza/303/247/303/243o do Ciclo de Vida do Projeto SADDLE.md" +0 -91
- package/docs/talks9/Mapeamento da Engrenagem Computacional_ Uma Arquitetura para Execu/303/247/303/243o Isolada e Persist/303/252ncia em Ambientes Distribu/303/255dos.md" +0 -116
- package/docs/talks9/O Cen/303/241rio Pr/303/241tico do SADDLE_ Uma An/303/241lise de Viabilidade e Modelo de Ciclo de Vida Integrado.md" +0 -128
- package/docs/talks9/README (2).md +0 -489
- package/docs/talks9/README.md +0 -198
- package/docs/talks9/Viabilidade do Saddle_ Uma An/303/241lise T/303/251cnica da Transforma/303/247/303/243o de Armazenamento Remoto em Mem/303/263ria Computacional.md" +0 -80
- package/docs/talks9/conversa.txt +0 -544
- package/docs/talks9/other (2).md +0 -39
- package/docs/talks9/other.md +0 -57
- package/docs/talks9/outro.txt +0 -24
- /package/{extension/README.md → docs/extension.md} +0 -0
|
@@ -1,1888 +0,0 @@
|
|
|
1
|
-
# Pesquisa: Batch Scraping e Concorrência (2026)
|
|
2
|
-
|
|
3
|
-
## Sumário
|
|
4
|
-
|
|
5
|
-
1. [Concurrent URL Scraping](#1-concurrent-url-scraping)
|
|
6
|
-
2. [Progress Tracking](#2-progress-tracking)
|
|
7
|
-
3. [Browser Pooling](#3-browser-pooling)
|
|
8
|
-
4. [Crawlee AutoscaledPool](#4-crawlee-autoscaledpool)
|
|
9
|
-
5. [Work Stealing/Distribution Patterns](#5-work-stealingdistribution-patterns)
|
|
10
|
-
6. [Graceful Degradation](#6-graceful-degradation)
|
|
11
|
-
7. [Layered Resilience](#7-layered-resilience)
|
|
12
|
-
8. [Adaptive Concurrency](#8-adaptive-concurrency)
|
|
13
|
-
9. [Complete Code Examples](#9-complete-code-examples)
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
## 1. Concurrent URL Scraping
|
|
18
|
-
|
|
19
|
-
### p-limit 7.3.0 (Simplest)
|
|
20
|
-
|
|
21
|
-
The simplest way to limit concurrency. Returns a wrapper function that limits how many promises run simultaneously.
|
|
22
|
-
|
|
23
|
-
```typescript
|
|
24
|
-
import pLimit from 'p-limit';
|
|
25
|
-
|
|
26
|
-
// Create a limiter with max 5 concurrent operations
|
|
27
|
-
const limit = pLimit(5);
|
|
28
|
-
|
|
29
|
-
async function scrapeUrl(url: string): Promise<ScrapedItem> {
|
|
30
|
-
const html = await fetch(url).then(r => r.text());
|
|
31
|
-
return parseHtml(html);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Usage
|
|
35
|
-
const urls = [
|
|
36
|
-
'https://example.com/1',
|
|
37
|
-
'https://example.com/2',
|
|
38
|
-
'https://example.com/3',
|
|
39
|
-
'https://example.com/4',
|
|
40
|
-
'https://example.com/5',
|
|
41
|
-
'https://example.com/6',
|
|
42
|
-
'https://example.com/7',
|
|
43
|
-
'https://example.com/8',
|
|
44
|
-
'https://example.com/9',
|
|
45
|
-
'https://example.com/10',
|
|
46
|
-
];
|
|
47
|
-
|
|
48
|
-
// limit() wraps each call — only 5 run at a time
|
|
49
|
-
const results = await Promise.all(
|
|
50
|
-
urls.map(url => limit(() => scrapeUrl(url)))
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
// You can also use limit.activeCount and limit.pendingCount
|
|
54
|
-
console.log(`Active: ${limit.activeCount}, Pending: ${limit.pendingCount}`);
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
### p-map 7.0.4 (Best for Mapping)
|
|
58
|
-
|
|
59
|
-
Like `Array.map()` but with concurrency control. Great for one-shot batch processing.
|
|
60
|
-
|
|
61
|
-
```typescript
|
|
62
|
-
import pMap from 'p-map';
|
|
63
|
-
|
|
64
|
-
const urls = [
|
|
65
|
-
'https://example.com/1',
|
|
66
|
-
'https://example.com/2',
|
|
67
|
-
'https://example.com/3',
|
|
68
|
-
];
|
|
69
|
-
|
|
70
|
-
// Map with concurrency of 3
|
|
71
|
-
const results = await pMap(
|
|
72
|
-
urls,
|
|
73
|
-
async (url) => {
|
|
74
|
-
const html = await fetch(url).then(r => r.text());
|
|
75
|
-
return parseHtml(html);
|
|
76
|
-
},
|
|
77
|
-
{ concurrency: 3 }
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
// With signal (abort support)
|
|
81
|
-
const controller = new AbortController();
|
|
82
|
-
|
|
83
|
-
const results = await pMap(
|
|
84
|
-
urls,
|
|
85
|
-
async (url) => {
|
|
86
|
-
const html = await fetch(url, { signal: controller.signal }).then(r => r.text());
|
|
87
|
-
return parseHtml(html);
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
concurrency: 5,
|
|
91
|
-
signal: controller.signal,
|
|
92
|
-
// Stop on first error (optional)
|
|
93
|
-
stopOnError: true,
|
|
94
|
-
}
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
// With individual error handling
|
|
98
|
-
const results = await pMap(
|
|
99
|
-
urls,
|
|
100
|
-
async (url) => {
|
|
101
|
-
try {
|
|
102
|
-
return await scrapeUrl(url);
|
|
103
|
-
} catch (error) {
|
|
104
|
-
console.error(`Failed: ${url}`, error);
|
|
105
|
-
return null; // Skip failed URLs
|
|
106
|
-
}
|
|
107
|
-
},
|
|
108
|
-
{ concurrency: 5 }
|
|
109
|
-
);
|
|
110
|
-
|
|
111
|
-
const successful = results.filter((r): r is ScrapedItem => r !== null);
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
### p-queue 9.1.2 (Best for Dynamic/Streaming)
|
|
115
|
-
|
|
116
|
-
A priority queue with concurrency control. Best when URLs arrive dynamically or you need priorities.
|
|
117
|
-
|
|
118
|
-
```typescript
|
|
119
|
-
import PQueue from 'p-queue';
|
|
120
|
-
|
|
121
|
-
// Create queue with concurrency of 5
|
|
122
|
-
const queue = new PQueue({
|
|
123
|
-
concurrency: 5,
|
|
124
|
-
intervalCap: 10, // Max operations per interval
|
|
125
|
-
interval: 1000, // Interval in ms (rate limiting)
|
|
126
|
-
timeout: 30_000, // Timeout for each item
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
// Add tasks with priority (lower = higher priority)
|
|
130
|
-
queue.add(
|
|
131
|
-
() => scrapeUrl('https://example.com/critical'),
|
|
132
|
-
{ priority: 1 } // High priority
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
queue.add(
|
|
136
|
-
() => scrapeUrl('https://example.com/normal'),
|
|
137
|
-
{ priority: 5 } // Normal priority
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
queue.add(
|
|
141
|
-
() => scrapeUrl('https://example.com/low'),
|
|
142
|
-
{ priority: 10 } // Low priority
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
// Dynamic URL submission
|
|
146
|
-
async function processUrlStream(urlStream: AsyncIterable<string>) {
|
|
147
|
-
for await (const url of urlStream) {
|
|
148
|
-
queue.add(() => scrapeUrl(url));
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Wait for completion
|
|
153
|
-
await queue.onIdle();
|
|
154
|
-
console.log(`Processed ${queue.size} items`);
|
|
155
|
-
|
|
156
|
-
// Event listeners
|
|
157
|
-
queue.on('active', () => {
|
|
158
|
-
console.log(`Active: ${queue.active}, Pending: ${queue.pending}`);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
queue.on('completed', (result) => {
|
|
162
|
-
console.log('Task completed');
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
queue.on('error', (error) => {
|
|
166
|
-
console.error('Task failed:', error);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
queue.on('idle', () => {
|
|
170
|
-
console.log('All tasks completed');
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
// Pause/resume
|
|
174
|
-
queue.pause();
|
|
175
|
-
// ... later
|
|
176
|
-
queue.resume();
|
|
177
|
-
|
|
178
|
-
// Clear pending tasks
|
|
179
|
-
queue.clear();
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
### Comparison
|
|
183
|
-
|
|
184
|
-
| Feature | p-limit | p-map | p-queue |
|
|
185
|
-
|---------|---------|-------|---------|
|
|
186
|
-
| Use case | Wrap individual calls | Map array with concurrency | Dynamic task queue |
|
|
187
|
-
| Priority | ❌ | ❌ | ✅ |
|
|
188
|
-
| Rate limiting | ❌ | ❌ | ✅ |
|
|
189
|
-
| Abort signal | ❌ | ✅ | ✅ |
|
|
190
|
-
| Events | ❌ | ❌ | ✅ |
|
|
191
|
-
| Pause/resume | ❌ | ❌ | ✅ |
|
|
192
|
-
| Size | ~1KB | ~2KB | ~4KB |
|
|
193
|
-
| Complexity | Simple | Simple | Moderate |
|
|
194
|
-
|
|
195
|
-
**Recommendation**: Use `p-map` for batch scraping (simplest), `p-queue` for complex workflows, `p-limit` when you need to wrap existing code.
|
|
196
|
-
|
|
197
|
-
---
|
|
198
|
-
|
|
199
|
-
## 2. Progress Tracking
|
|
200
|
-
|
|
201
|
-
### cli-progress 3.12.0 (Terminal Progress Bars)
|
|
202
|
-
|
|
203
|
-
```typescript
|
|
204
|
-
import cliProgress from 'cli-progress';
|
|
205
|
-
|
|
206
|
-
// Create progress bar
|
|
207
|
-
const progressBar = new cliProgress.SingleBar(
|
|
208
|
-
{
|
|
209
|
-
format: 'Scraping |{bar}| {percentage}% | {value}/{total} | {url} | Speed: {speed} url/s | ETA: {eta_formatted}',
|
|
210
|
-
barCompleteChar: '\u2588',
|
|
211
|
-
barIncompleteChar: '\u2591',
|
|
212
|
-
hideCursor: true,
|
|
213
|
-
clearOnComplete: false,
|
|
214
|
-
stopOnComplete: true,
|
|
215
|
-
etaBuffer: 10, // Calculate ETA from last 10 updates
|
|
216
|
-
},
|
|
217
|
-
cliProgress.Presets.shades_classic
|
|
218
|
-
);
|
|
219
|
-
|
|
220
|
-
// Start with total
|
|
221
|
-
progressBar.start(totalUrls, 0, { url: '', speed: 0 });
|
|
222
|
-
|
|
223
|
-
// Update for each completed URL
|
|
224
|
-
for (const url of urls) {
|
|
225
|
-
try {
|
|
226
|
-
const result = await scrapeUrl(url);
|
|
227
|
-
progressBar.increment(1, {
|
|
228
|
-
url: truncateUrl(url, 40),
|
|
229
|
-
speed: (progressBar.getCurrent() / ((Date.now() - startTime) / 1000)).toFixed(1),
|
|
230
|
-
});
|
|
231
|
-
} catch (error) {
|
|
232
|
-
progressBar.increment(1, { url: `FAILED: ${truncateUrl(url, 30)}` });
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
progressBar.stop();
|
|
237
|
-
|
|
238
|
-
function truncateUrl(url: string, maxLen: number): string {
|
|
239
|
-
try {
|
|
240
|
-
const parsed = new URL(url);
|
|
241
|
-
const path = parsed.pathname + parsed.search;
|
|
242
|
-
if (path.length > maxLen) return path.slice(0, maxLen - 3) + '...';
|
|
243
|
-
return path;
|
|
244
|
-
} catch {
|
|
245
|
-
return url.slice(0, maxLen);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
### Multi-Progress Bars
|
|
251
|
-
|
|
252
|
-
```typescript
|
|
253
|
-
import MultiProgress from 'multi-progress';
|
|
254
|
-
import cliProgress from 'cli-progress';
|
|
255
|
-
|
|
256
|
-
const multi = new MultiProgress(process.stderr);
|
|
257
|
-
|
|
258
|
-
// Multiple concurrent progress bars
|
|
259
|
-
const bars = new Map<string, cliProgress.SingleBar>();
|
|
260
|
-
|
|
261
|
-
function createBar(name: string, total: number): cliProgress.SingleBar {
|
|
262
|
-
const bar = multi.newBar(
|
|
263
|
-
`${name} |{bar}| {percentage}% | {value}/{total}`,
|
|
264
|
-
{
|
|
265
|
-
complete: '\u2588',
|
|
266
|
-
incomplete: '\u2591',
|
|
267
|
-
width: 30,
|
|
268
|
-
}
|
|
269
|
-
);
|
|
270
|
-
bar.start(total, 0);
|
|
271
|
-
bars.set(name, bar);
|
|
272
|
-
return bar;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Usage
|
|
276
|
-
const mainBar = createBar('Total ', urls.length);
|
|
277
|
-
const successBar = createBar('Success ', 0);
|
|
278
|
-
const errorBar = createBar('Errors ', 0);
|
|
279
|
-
|
|
280
|
-
// Process with p-limit
|
|
281
|
-
const limit = pLimit(5);
|
|
282
|
-
|
|
283
|
-
await Promise.all(
|
|
284
|
-
urls.map(url =>
|
|
285
|
-
limit(async () => {
|
|
286
|
-
try {
|
|
287
|
-
await scrapeUrl(url);
|
|
288
|
-
mainBar.increment(1);
|
|
289
|
-
successBar.increment(1);
|
|
290
|
-
} catch {
|
|
291
|
-
mainBar.increment(1);
|
|
292
|
-
errorBar.increment(1);
|
|
293
|
-
}
|
|
294
|
-
})
|
|
295
|
-
)
|
|
296
|
-
);
|
|
297
|
-
|
|
298
|
-
// Stop all bars
|
|
299
|
-
bars.forEach(bar => bar.stop());
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
### ora (Spinner for Single Operations)
|
|
303
|
-
|
|
304
|
-
```typescript
|
|
305
|
-
import ora from 'ora';
|
|
306
|
-
|
|
307
|
-
const spinner = ora('Starting scraper...').start();
|
|
308
|
-
|
|
309
|
-
try {
|
|
310
|
-
// Phase 1
|
|
311
|
-
spinner.text = 'Launching browser...';
|
|
312
|
-
const browser = await launchBrowser();
|
|
313
|
-
spinner.succeed('Browser launched');
|
|
314
|
-
|
|
315
|
-
// Phase 2
|
|
316
|
-
spinner.start('Scraping URLs...');
|
|
317
|
-
let completed = 0;
|
|
318
|
-
for (const url of urls) {
|
|
319
|
-
await scrapeUrl(url);
|
|
320
|
-
completed++;
|
|
321
|
-
spinner.text = `Scraping... (${completed}/${urls.length})`;
|
|
322
|
-
}
|
|
323
|
-
spinner.succeed(`Scraped ${urls.length} URLs`);
|
|
324
|
-
|
|
325
|
-
} catch (error) {
|
|
326
|
-
spinner.fail(`Failed: ${error.message}`);
|
|
327
|
-
}
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
### Combined Progress Pattern
|
|
331
|
-
|
|
332
|
-
```typescript
|
|
333
|
-
import cliProgress from 'cli-progress';
|
|
334
|
-
import ora from 'ora';
|
|
335
|
-
import PQueue from 'p-queue';
|
|
336
|
-
|
|
337
|
-
interface ProgressState {
|
|
338
|
-
total: number;
|
|
339
|
-
completed: number;
|
|
340
|
-
succeeded: number;
|
|
341
|
-
failed: number;
|
|
342
|
-
activeRequests: number;
|
|
343
|
-
startTime: number;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
class ProgressTracker {
|
|
347
|
-
private bar: cliProgress.SingleBar;
|
|
348
|
-
private spinner: ora.Ora;
|
|
349
|
-
private state: ProgressState;
|
|
350
|
-
|
|
351
|
-
constructor(total: number) {
|
|
352
|
-
this.state = {
|
|
353
|
-
total,
|
|
354
|
-
completed: 0,
|
|
355
|
-
succeeded: 0,
|
|
356
|
-
failed: 0,
|
|
357
|
-
activeRequests: 0,
|
|
358
|
-
startTime: Date.now(),
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
this.bar = new cliProgress.SingleBar({
|
|
362
|
-
format: 'Progress |{bar}| {percentage}% | {value}/{total} | ✓ {succeeded} ✗ {failed} | {speed} url/s',
|
|
363
|
-
barCompleteChar: '\u2588',
|
|
364
|
-
barIncompleteChar: '\u2591',
|
|
365
|
-
hideCursor: true,
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
this.spinner = ora({ spinner: 'dots' });
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
start(): void {
|
|
372
|
-
this.bar.start(this.state.total, 0);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
update(type: 'success' | 'error'): void {
|
|
376
|
-
this.state.completed++;
|
|
377
|
-
if (type === 'success') this.state.succeeded++;
|
|
378
|
-
else this.state.failed++;
|
|
379
|
-
|
|
380
|
-
const elapsed = (Date.now() - this.state.startTime) / 1000;
|
|
381
|
-
const speed = (this.state.completed / elapsed).toFixed(1);
|
|
382
|
-
|
|
383
|
-
this.bar.update(this.state.completed, {
|
|
384
|
-
succeeded: this.state.succeeded,
|
|
385
|
-
failed: this.state.failed,
|
|
386
|
-
speed,
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
setActiveRequests(count: number): void {
|
|
391
|
-
this.state.activeRequests = count;
|
|
392
|
-
this.spinner.text = `Active requests: ${count}`;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
stop(): void {
|
|
396
|
-
this.bar.stop();
|
|
397
|
-
const elapsed = ((Date.now() - this.state.startTime) / 1000).toFixed(1);
|
|
398
|
-
console.log(`\nCompleted in ${elapsed}s`);
|
|
399
|
-
console.log(` Total: ${this.state.total}`);
|
|
400
|
-
console.log(` Succeeded: ${this.state.succeeded}`);
|
|
401
|
-
console.log(` Failed: ${this.state.failed}`);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// Usage
|
|
406
|
-
const progress = new ProgressTracker(urls.length);
|
|
407
|
-
progress.start();
|
|
408
|
-
|
|
409
|
-
const queue = new PQueue({ concurrency: 5 });
|
|
410
|
-
|
|
411
|
-
for (const url of urls) {
|
|
412
|
-
queue.add(async () => {
|
|
413
|
-
try {
|
|
414
|
-
await scrapeUrl(url);
|
|
415
|
-
progress.update('success');
|
|
416
|
-
} catch {
|
|
417
|
-
progress.update('error');
|
|
418
|
-
}
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
await queue.onIdle();
|
|
423
|
-
progress.stop();
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
---
|
|
427
|
-
|
|
428
|
-
## 3. Browser Pooling
|
|
429
|
-
|
|
430
|
-
### Core Pattern: Acquire/Release
|
|
431
|
-
|
|
432
|
-
```typescript
|
|
433
|
-
import { chromium, type Browser, type BrowserContext } from 'playwright';
|
|
434
|
-
|
|
435
|
-
interface BrowserPoolOptions {
|
|
436
|
-
minBrowsers: number;
|
|
437
|
-
maxBrowsers: number;
|
|
438
|
-
maxPagesPerBrowser: number;
|
|
439
|
-
idleTimeout: number;
|
|
440
|
-
launchTimeout: number;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
interface PooledBrowser {
|
|
444
|
-
browser: Browser;
|
|
445
|
-
contexts: BrowserContext[];
|
|
446
|
-
lastUsed: number;
|
|
447
|
-
busy: boolean;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
class BrowserPool {
|
|
451
|
-
private browsers: PooledBrowser[] = [];
|
|
452
|
-
private waitQueue: Array<{
|
|
453
|
-
resolve: (context: BrowserContext) => void;
|
|
454
|
-
reject: (error: Error) => void;
|
|
455
|
-
timeout: ReturnType<typeof setTimeout>;
|
|
456
|
-
}> = [];
|
|
457
|
-
private options: BrowserPoolOptions;
|
|
458
|
-
private cleanupInterval: ReturnType<typeof setInterval>;
|
|
459
|
-
|
|
460
|
-
constructor(options: Partial<BrowserPoolOptions> = {}) {
|
|
461
|
-
this.options = {
|
|
462
|
-
minBrowsers: 1,
|
|
463
|
-
maxBrowsers: 5,
|
|
464
|
-
maxPagesPerBrowser: 10,
|
|
465
|
-
idleTimeout: 60_000,
|
|
466
|
-
launchTimeout: 30_000,
|
|
467
|
-
...options,
|
|
468
|
-
};
|
|
469
|
-
|
|
470
|
-
// Pre-launch minimum browsers
|
|
471
|
-
this.initialize();
|
|
472
|
-
|
|
473
|
-
// Cleanup idle browsers periodically
|
|
474
|
-
this.cleanupInterval = setInterval(
|
|
475
|
-
() => this.cleanupIdleBrowsers(),
|
|
476
|
-
this.options.idleTimeout / 2
|
|
477
|
-
);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
private async initialize(): Promise<void> {
|
|
481
|
-
for (let i = 0; i < this.options.minBrowsers; i++) {
|
|
482
|
-
await this.launchBrowser();
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
private async launchBrowser(): Promise<PooledBrowser> {
|
|
487
|
-
const browser = await chromium.launch({
|
|
488
|
-
headless: true,
|
|
489
|
-
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
const pooled: PooledBrowser = {
|
|
493
|
-
browser,
|
|
494
|
-
contexts: [],
|
|
495
|
-
lastUsed: Date.now(),
|
|
496
|
-
busy: false,
|
|
497
|
-
};
|
|
498
|
-
|
|
499
|
-
this.browsers.push(pooled);
|
|
500
|
-
return pooled;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
private findAvailableBrowser(): PooledBrowser | null {
|
|
504
|
-
// Find a browser with available pages
|
|
505
|
-
for (const pooled of this.browsers) {
|
|
506
|
-
if (!pooled.busy && pooled.contexts.length < this.options.maxPagesPerBrowser) {
|
|
507
|
-
return pooled;
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
return null;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
private async waitForBrowser(): Promise<PooledBrowser> {
|
|
514
|
-
return new Promise(async (resolve, reject) => {
|
|
515
|
-
const timeout = setTimeout(() => {
|
|
516
|
-
const idx = this.waitQueue.findIndex(w => w.resolve === resolve);
|
|
517
|
-
if (idx !== -1) this.waitQueue.splice(idx, 1);
|
|
518
|
-
reject(new Error('Timeout waiting for available browser'));
|
|
519
|
-
}, this.options.launchTimeout);
|
|
520
|
-
|
|
521
|
-
this.waitQueue.push({ resolve, reject, timeout });
|
|
522
|
-
|
|
523
|
-
// Try to launch a new browser if under limit
|
|
524
|
-
if (this.browsers.length < this.options.maxBrowsers) {
|
|
525
|
-
try {
|
|
526
|
-
const pooled = await this.launchBrowser();
|
|
527
|
-
// Resolve one waiter with this new browser
|
|
528
|
-
const waiter = this.waitQueue.shift();
|
|
529
|
-
if (waiter) {
|
|
530
|
-
clearTimeout(waiter.timeout);
|
|
531
|
-
waiter.resolve(pooled);
|
|
532
|
-
}
|
|
533
|
-
} catch (error) {
|
|
534
|
-
console.error('Failed to launch browser:', error);
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
});
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
/**
|
|
541
|
-
* Acquire a browser context for scraping.
|
|
542
|
-
* Returns a context that must be released when done.
|
|
543
|
-
*/
|
|
544
|
-
async acquire(): Promise<{ context: BrowserContext; release: () => Promise<void> }> {
|
|
545
|
-
let pooled = this.findAvailableBrowser();
|
|
546
|
-
|
|
547
|
-
if (!pooled) {
|
|
548
|
-
pooled = await this.waitForBrowser();
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
const context = await pooled.browser.newContext({
|
|
552
|
-
userAgent: this.getRandomUserAgent(),
|
|
553
|
-
viewport: {
|
|
554
|
-
width: 1920 + Math.floor(Math.random() * 100),
|
|
555
|
-
height: 1080 + Math.floor(Math.random() * 100),
|
|
556
|
-
},
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
pooled.contexts.push(context);
|
|
560
|
-
pooled.lastUsed = Date.now();
|
|
561
|
-
|
|
562
|
-
let released = false;
|
|
563
|
-
const release = async (): Promise<void> => {
|
|
564
|
-
if (released) return;
|
|
565
|
-
released = true;
|
|
566
|
-
|
|
567
|
-
try {
|
|
568
|
-
await context.close();
|
|
569
|
-
} catch {
|
|
570
|
-
// Context may already be closed
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
pooled.contexts = pooled.contexts.filter(c => c !== context);
|
|
574
|
-
pooled.lastUsed = Date.now();
|
|
575
|
-
|
|
576
|
-
// Check if any waiters need this browser
|
|
577
|
-
if (this.waitQueue.length > 0 && pooled.contexts.length < this.options.maxPagesPerBrowser) {
|
|
578
|
-
const waiter = this.waitQueue.shift();
|
|
579
|
-
if (waiter) {
|
|
580
|
-
clearTimeout(waiter.timeout);
|
|
581
|
-
waiter.resolve(pooled);
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
};
|
|
585
|
-
|
|
586
|
-
return { context, release };
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
private async cleanupIdleBrowsers(): Promise<void> {
|
|
590
|
-
const now = Date.now();
|
|
591
|
-
const toRemove: PooledBrowser[] = [];
|
|
592
|
-
|
|
593
|
-
for (const pooled of this.browsers) {
|
|
594
|
-
if (
|
|
595
|
-
pooled.contexts.length === 0 &&
|
|
596
|
-
now - pooled.lastUsed > this.options.idleTimeout &&
|
|
597
|
-
this.browsers.length - toRemove.length > this.options.minBrowsers
|
|
598
|
-
) {
|
|
599
|
-
toRemove.push(pooled);
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
for (const pooled of toRemove) {
|
|
604
|
-
try {
|
|
605
|
-
await pooled.browser.close();
|
|
606
|
-
} catch {
|
|
607
|
-
// Ignore close errors
|
|
608
|
-
}
|
|
609
|
-
this.browsers = this.browsers.filter(b => b !== pooled);
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
/**
|
|
614
|
-
* Graceful cleanup — close all browsers and reject pending waiters.
|
|
615
|
-
*/
|
|
616
|
-
async destroy(): Promise<void> {
|
|
617
|
-
clearInterval(this.cleanupInterval);
|
|
618
|
-
|
|
619
|
-
// Reject all pending waiters
|
|
620
|
-
for (const waiter of this.waitQueue) {
|
|
621
|
-
clearTimeout(waiter.timeout);
|
|
622
|
-
waiter.reject(new Error('Browser pool is shutting down'));
|
|
623
|
-
}
|
|
624
|
-
this.waitQueue = [];
|
|
625
|
-
|
|
626
|
-
// Close all browsers
|
|
627
|
-
for (const pooled of this.browsers) {
|
|
628
|
-
for (const ctx of pooled.contexts) {
|
|
629
|
-
try {
|
|
630
|
-
await ctx.close();
|
|
631
|
-
} catch { /* ignore */ }
|
|
632
|
-
}
|
|
633
|
-
try {
|
|
634
|
-
await pooled.browser.close();
|
|
635
|
-
} catch { /* ignore */ }
|
|
636
|
-
}
|
|
637
|
-
this.browsers = [];
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
private getRandomUserAgent(): string {
|
|
641
|
-
const agents = [
|
|
642
|
-
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0',
|
|
643
|
-
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/120.0.0.0',
|
|
644
|
-
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/120.0.0.0',
|
|
645
|
-
];
|
|
646
|
-
return agents[Math.floor(Math.random() * agents.length)];
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
get stats() {
|
|
650
|
-
return {
|
|
651
|
-
totalBrowsers: this.browsers.length,
|
|
652
|
-
activeBrowsers: this.browsers.filter(b => b.contexts.length > 0).length,
|
|
653
|
-
totalContexts: this.browsers.reduce((sum, b) => sum + b.contexts.length, 0),
|
|
654
|
-
pendingRequests: this.waitQueue.length,
|
|
655
|
-
};
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
// Usage
|
|
660
|
-
const pool = new BrowserPool({ maxBrowsers: 3, maxPagesPerBrowser: 5 });
|
|
661
|
-
|
|
662
|
-
async function scrapeWithBrowser(url: string): Promise<string> {
|
|
663
|
-
const { context, release } = await pool.acquire();
|
|
664
|
-
|
|
665
|
-
try {
|
|
666
|
-
const page = await context.newPage();
|
|
667
|
-
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
|
|
668
|
-
const content = await page.content();
|
|
669
|
-
return content;
|
|
670
|
-
} finally {
|
|
671
|
-
await release();
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
// Process URLs with browser pool
|
|
676
|
-
const limit = pLimit(5);
|
|
677
|
-
|
|
678
|
-
await Promise.all(
|
|
679
|
-
urls.map(url =>
|
|
680
|
-
limit(async () => {
|
|
681
|
-
const html = await scrapeWithBrowser(url);
|
|
682
|
-
return parseHtml(html);
|
|
683
|
-
})
|
|
684
|
-
)
|
|
685
|
-
);
|
|
686
|
-
|
|
687
|
-
// Graceful shutdown
|
|
688
|
-
await pool.destroy();
|
|
689
|
-
```
|
|
690
|
-
|
|
691
|
-
---
|
|
692
|
-
|
|
693
|
-
## 4. Crawlee AutoscaledPool
|
|
694
|
-
|
|
695
|
-
Crawlee's `AutoscaledPool` dynamically adjusts concurrency based on system load and success rate. It's the gold standard for adaptive scraping.
|
|
696
|
-
|
|
697
|
-
### Core Concepts
|
|
698
|
-
|
|
699
|
-
```typescript
|
|
700
|
-
import { AutoscaledPool } from 'crawlee';
|
|
701
|
-
|
|
702
|
-
const pool = new AutoscaledPool({
|
|
703
|
-
// Min/max concurrency bounds
|
|
704
|
-
minConcurrency: 1,
|
|
705
|
-
maxConcurrency: 20,
|
|
706
|
-
|
|
707
|
-
// System metrics
|
|
708
|
-
snapshotterOptions: {
|
|
709
|
-
// CPU/memory thresholds
|
|
710
|
-
cpuSnapshotInterval: 1000,
|
|
711
|
-
memSnapshotInterval: 2000,
|
|
712
|
-
},
|
|
713
|
-
|
|
714
|
-
// Scaling behavior
|
|
715
|
-
scaleUpInterval: 1000, // Check every 1s
|
|
716
|
-
scaleDownInterval: 5000, // Check every 5s
|
|
717
|
-
maxScaledUpRate: 2, // Max 2x scale-up per interval
|
|
718
|
-
maxScaledDownRate: 0.5, // Max 0.5x scale-down per interval
|
|
719
|
-
|
|
720
|
-
// Task function
|
|
721
|
-
taskFunction: async () => {
|
|
722
|
-
const url = getNextUrl();
|
|
723
|
-
if (!url) return false; // No more tasks
|
|
724
|
-
|
|
725
|
-
try {
|
|
726
|
-
await scrapeUrl(url);
|
|
727
|
-
return true; // Task completed
|
|
728
|
-
} catch (error) {
|
|
729
|
-
if (isRetryable(error)) {
|
|
730
|
-
// Return the URL to the queue
|
|
731
|
-
addUrlBack(url);
|
|
732
|
-
return true; // Task "completed" (will be retried)
|
|
733
|
-
}
|
|
734
|
-
throw error; // Non-retryable error
|
|
735
|
-
}
|
|
736
|
-
},
|
|
737
|
-
});
|
|
738
|
-
|
|
739
|
-
// Start the pool
|
|
740
|
-
await pool.run();
|
|
741
|
-
```
|
|
742
|
-
|
|
743
|
-
### How AutoscaledPool Works
|
|
744
|
-
|
|
745
|
-
1. **Snapshotter**: Continuously monitors CPU and memory usage
|
|
746
|
-
2. **Scaler**: Adjusts concurrency based on metrics:
|
|
747
|
-
- If CPU < 50% and memory < 70% → scale up
|
|
748
|
-
- If CPU > 80% or memory > 85% → scale down
|
|
749
|
-
3. **Task runner**: Runs tasks at current concurrency level
|
|
750
|
-
4. **Idle detection**: Stops when no more tasks are available
|
|
751
|
-
|
|
752
|
-
### Custom Scaling Logic
|
|
753
|
-
|
|
754
|
-
```typescript
|
|
755
|
-
import { AutoscaledPool, AutoscaledPoolOptions } from 'crawlee';
|
|
756
|
-
|
|
757
|
-
class SmartAutoscaledPool extends AutoscaledPool {
|
|
758
|
-
private errorRate = 0;
|
|
759
|
-
private recentErrors = 0;
|
|
760
|
-
private recentSuccesses = 0;
|
|
761
|
-
|
|
762
|
-
constructor(options: AutoscaledPoolOptions) {
|
|
763
|
-
super({
|
|
764
|
-
...options,
|
|
765
|
-
// Custom scaling logic
|
|
766
|
-
desiredConcurrencyCallback: () => {
|
|
767
|
-
// If error rate is high, reduce concurrency
|
|
768
|
-
if (this.errorRate > 0.3) {
|
|
769
|
-
return Math.max(1, this.desiredConcurrency - 2);
|
|
770
|
-
}
|
|
771
|
-
// If error rate is low and system is healthy, increase
|
|
772
|
-
if (this.errorRate < 0.1 && this.isSystemHealthy()) {
|
|
773
|
-
return Math.min(
|
|
774
|
-
this.maxConcurrency,
|
|
775
|
-
this.desiredConcurrency + 1
|
|
776
|
-
);
|
|
777
|
-
}
|
|
778
|
-
return this.desiredConcurrency;
|
|
779
|
-
},
|
|
780
|
-
});
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
trackSuccess(): void {
|
|
784
|
-
this.recentSuccesses++;
|
|
785
|
-
this.updateErrorRate();
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
trackError(): void {
|
|
789
|
-
this.recentErrors++;
|
|
790
|
-
this.updateErrorRate();
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
private updateErrorRate(): void {
|
|
794
|
-
const total = this.recentErrors + this.recentSuccesses;
|
|
795
|
-
if (total >= 100) {
|
|
796
|
-
this.errorRate = this.recentErrors / total;
|
|
797
|
-
this.recentErrors = 0;
|
|
798
|
-
this.recentSuccesses = 0;
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
private isSystemHealthy(): boolean {
|
|
803
|
-
// Check CPU and memory
|
|
804
|
-
const memUsage = process.memoryUsage();
|
|
805
|
-
const memPercent = memUsage.heapUsed / memUsage.heapTotal;
|
|
806
|
-
return memPercent < 0.7;
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
```
|
|
810
|
-
|
|
811
|
-
---
|
|
812
|
-
|
|
813
|
-
## 5. Work Stealing/Distribution Patterns
|
|
814
|
-
|
|
815
|
-
### Producer-Consumer Pattern
|
|
816
|
-
|
|
817
|
-
```typescript
|
|
818
|
-
import PQueue from 'p-queue';
|
|
819
|
-
import { EventEmitter } from 'events';
|
|
820
|
-
|
|
821
|
-
interface ScrapingTask {
|
|
822
|
-
id: string;
|
|
823
|
-
url: string;
|
|
824
|
-
priority: number;
|
|
825
|
-
retries: number;
|
|
826
|
-
maxRetries: number;
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
class WorkDistributor extends EventEmitter {
|
|
830
|
-
private queue: PQueue;
|
|
831
|
-
private workers: Map<string, Worker> = new Map();
|
|
832
|
-
private tasks: Map<string, ScrapingTask> = new Map();
|
|
833
|
-
|
|
834
|
-
constructor(concurrency: number = 10) {
|
|
835
|
-
super();
|
|
836
|
-
this.queue = new PQueue({ concurrency });
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
addTask(task: ScrapingTask): void {
|
|
840
|
-
this.tasks.set(task.id, task);
|
|
841
|
-
this.queue.add(() => this.executeTask(task));
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
addTasks(tasks: ScrapingTask[]): void {
|
|
845
|
-
tasks.forEach(t => this.addTask(t));
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
private async executeTask(task: ScrapingTask): Promise<void> {
|
|
849
|
-
this.emit('task:start', task);
|
|
850
|
-
|
|
851
|
-
try {
|
|
852
|
-
const result = await scrapeUrl(task.url);
|
|
853
|
-
this.tasks.delete(task.id);
|
|
854
|
-
this.emit('task:complete', { task, result });
|
|
855
|
-
} catch (error) {
|
|
856
|
-
if (task.retries < task.maxRetries) {
|
|
857
|
-
// Re-queue with incremented retry count
|
|
858
|
-
task.retries++;
|
|
859
|
-
this.emit('task:retry', { task, attempt: task.retries, error });
|
|
860
|
-
this.queue.add(() => this.executeTask(task));
|
|
861
|
-
} else {
|
|
862
|
-
this.tasks.delete(task.id);
|
|
863
|
-
this.emit('task:failed', { task, error });
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
get stats() {
|
|
869
|
-
return {
|
|
870
|
-
pending: this.queue.pending,
|
|
871
|
-
active: this.queue.active,
|
|
872
|
-
totalTasks: this.tasks.size,
|
|
873
|
-
isIdle: this.queue.idle,
|
|
874
|
-
};
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
async drain(): Promise<void> {
|
|
878
|
-
await this.queue.onIdle();
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
// Usage
|
|
883
|
-
const distributor = new WorkDistributor(10);
|
|
884
|
-
|
|
885
|
-
distributor.on('task:complete', ({ task, result }) => {
|
|
886
|
-
console.log(`✓ ${task.url}`);
|
|
887
|
-
});
|
|
888
|
-
|
|
889
|
-
distributor.on('task:failed', ({ task, error }) => {
|
|
890
|
-
console.error(`✗ ${task.url}: ${error.message}`);
|
|
891
|
-
});
|
|
892
|
-
|
|
893
|
-
distributor.addTasks(
|
|
894
|
-
urls.map((url, i) => ({
|
|
895
|
-
id: `task-${i}`,
|
|
896
|
-
url,
|
|
897
|
-
priority: 5,
|
|
898
|
-
retries: 0,
|
|
899
|
-
maxRetries: 3,
|
|
900
|
-
}))
|
|
901
|
-
);
|
|
902
|
-
|
|
903
|
-
await distributor.drain();
|
|
904
|
-
```
|
|
905
|
-
|
|
906
|
-
### Domain-Based Distribution
|
|
907
|
-
|
|
908
|
-
Distribute work by domain to respect per-domain rate limits.
|
|
909
|
-
|
|
910
|
-
```typescript
|
|
911
|
-
import PQueue from 'p-queue';
|
|
912
|
-
|
|
913
|
-
class DomainDistributor {
|
|
914
|
-
private domainQueues = new Map<string, PQueue>();
|
|
915
|
-
private globalQueue: PQueue;
|
|
916
|
-
|
|
917
|
-
constructor(
|
|
918
|
-
private globalConcurrency: number = 50,
|
|
919
|
-
private perDomainConcurrency: number = 3
|
|
920
|
-
) {
|
|
921
|
-
this.globalQueue = new PQueue({ concurrency: globalConcurrency });
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
private getDomainQueue(domain: string): PQueue {
|
|
925
|
-
if (!this.domainQueues.has(domain)) {
|
|
926
|
-
const queue = new PQueue({
|
|
927
|
-
concurrency: this.perDomainConcurrency,
|
|
928
|
-
interval: 1000, // Rate limit: 1 request per second
|
|
929
|
-
intervalCap: 5, // Max 5 requests per interval
|
|
930
|
-
});
|
|
931
|
-
this.domainQueues.set(domain, queue);
|
|
932
|
-
}
|
|
933
|
-
return this.domainQueues.get(domain)!;
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
async add(url: string, task: () => Promise<unknown>): Promise<void> {
|
|
937
|
-
const domain = new URL(url).hostname;
|
|
938
|
-
const domainQueue = this.getDomainQueue(domain);
|
|
939
|
-
|
|
940
|
-
// Wrap domain-specific task in global queue
|
|
941
|
-
this.globalQueue.add(() => domainQueue.add(task));
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
async drain(): Promise<void> {
|
|
945
|
-
await this.globalQueue.onIdle();
|
|
946
|
-
// Wait for all domain queues too
|
|
947
|
-
for (const queue of this.domainQueues.values()) {
|
|
948
|
-
await queue.onIdle();
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
getStats() {
|
|
953
|
-
const domainStats: Record<string, { pending: number; active: number }> = {};
|
|
954
|
-
for (const [domain, queue] of this.domainQueues) {
|
|
955
|
-
domainStats[domain] = {
|
|
956
|
-
pending: queue.pending,
|
|
957
|
-
active: queue.active,
|
|
958
|
-
};
|
|
959
|
-
}
|
|
960
|
-
return {
|
|
961
|
-
global: {
|
|
962
|
-
pending: this.globalQueue.pending,
|
|
963
|
-
active: this.globalQueue.active,
|
|
964
|
-
},
|
|
965
|
-
domains: domainStats,
|
|
966
|
-
};
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
// Usage
|
|
971
|
-
const distributor = new DomainDistributor(50, 3);
|
|
972
|
-
|
|
973
|
-
await Promise.all(
|
|
974
|
-
urls.map(url =>
|
|
975
|
-
distributor.add(url, async () => {
|
|
976
|
-
const html = await fetch(url).then(r => r.text());
|
|
977
|
-
return parseHtml(html);
|
|
978
|
-
})
|
|
979
|
-
)
|
|
980
|
-
);
|
|
981
|
-
```
|
|
982
|
-
|
|
983
|
-
---
|
|
984
|
-
|
|
985
|
-
## 6. Graceful Degradation
|
|
986
|
-
|
|
987
|
-
### Circuit Breaker (cockatiel 4.0.0)
|
|
988
|
-
|
|
989
|
-
The circuit breaker pattern prevents cascading failures by stopping requests to a failing service.
|
|
990
|
-
|
|
991
|
-
```typescript
|
|
992
|
-
import { CircuitBreaker, handle } from 'cockatiel';
|
|
993
|
-
|
|
994
|
-
// Create a circuit breaker
|
|
995
|
-
const breaker = CircuitBreaker.byPercentage({
|
|
996
|
-
threshold: 50, // Open circuit when 50% of requests fail
|
|
997
|
-
duration: 30_000, // Stay open for 30 seconds
|
|
998
|
-
halfOpenAfter: 10_000, // Try again after 10 seconds
|
|
999
|
-
});
|
|
1000
|
-
|
|
1001
|
-
// Wrap fetch calls with the circuit breaker
|
|
1002
|
-
async function resilientFetch(url: string): Promise<Response> {
|
|
1003
|
-
return handle(
|
|
1004
|
-
breaker,
|
|
1005
|
-
() => fetch(url),
|
|
1006
|
-
(error) => {
|
|
1007
|
-
// Fallback when circuit is open
|
|
1008
|
-
console.warn(`Circuit open for ${new URL(url).hostname}, using fallback`);
|
|
1009
|
-
return new Response(JSON.stringify({ error: 'Service unavailable' }), {
|
|
1010
|
-
status: 503,
|
|
1011
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1012
|
-
});
|
|
1013
|
-
}
|
|
1014
|
-
).promise();
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
// Or with more control
|
|
1018
|
-
const circuit = CircuitBreaker.byPercentage({
|
|
1019
|
-
threshold: 50,
|
|
1020
|
-
duration: 30_000,
|
|
1021
|
-
halfOpenAfter: 10_000,
|
|
1022
|
-
});
|
|
1023
|
-
|
|
1024
|
-
// Listen for state changes
|
|
1025
|
-
circuit.onBreak(() => console.log('Circuit OPEN — requests blocked'));
|
|
1026
|
-
circuit.onReset(() => console.log('Circuit CLOSED — requests allowed'));
|
|
1027
|
-
circuit.onHalfOpen(() => console.log('Circuit HALF-OPEN — testing'));
|
|
1028
|
-
|
|
1029
|
-
// Manual circuit breaker
|
|
1030
|
-
async function fetchWithCircuitBreaker(url: string): Promise<Response> {
|
|
1031
|
-
const result = await handle(
|
|
1032
|
-
circuit,
|
|
1033
|
-
() => fetch(url),
|
|
1034
|
-
() => {
|
|
1035
|
-
// Fallback
|
|
1036
|
-
return new Response('{}', { status: 503 });
|
|
1037
|
-
}
|
|
1038
|
-
).promise();
|
|
1039
|
-
|
|
1040
|
-
return result;
|
|
1041
|
-
}
|
|
1042
|
-
```
|
|
1043
|
-
|
|
1044
|
-
### Bulkhead Isolation
|
|
1045
|
-
|
|
1046
|
-
Bulkhead pattern isolates failures to specific domains or operation types.
|
|
1047
|
-
|
|
1048
|
-
```typescript
|
|
1049
|
-
import PQueue from 'p-queue';
|
|
1050
|
-
|
|
1051
|
-
class BulkheadPool {
|
|
1052
|
-
private bulkheads = new Map<string, PQueue>();
|
|
1053
|
-
private maxPerBulkhead: number;
|
|
1054
|
-
private maxTotal: number;
|
|
1055
|
-
private totalActive = 0;
|
|
1056
|
-
|
|
1057
|
-
constructor(maxPerBulkhead: number = 5, maxTotal: number = 50) {
|
|
1058
|
-
this.maxPerBulkhead = maxPerBulkhead;
|
|
1059
|
-
this.maxTotal = maxTotal;
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
private getBulkhead(name: string): PQueue {
|
|
1063
|
-
if (!this.bulkheads.has(name)) {
|
|
1064
|
-
this.bulkheads.set(name, new PQueue({
|
|
1065
|
-
concurrency: this.maxPerBulkhead,
|
|
1066
|
-
}));
|
|
1067
|
-
}
|
|
1068
|
-
return this.bulkheads.get(name)!;
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
async execute<T>(bulkheadName: string, fn: () => Promise<T>): Promise<T> {
|
|
1072
|
-
if (this.totalActive >= this.maxTotal) {
|
|
1073
|
-
throw new Error('Total concurrency limit reached');
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
const bulkhead = this.getBulkhead(bulkheadName);
|
|
1077
|
-
|
|
1078
|
-
this.totalActive++;
|
|
1079
|
-
try {
|
|
1080
|
-
return await bulkhead.add(fn);
|
|
1081
|
-
} finally {
|
|
1082
|
-
this.totalActive--;
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
getStats() {
|
|
1087
|
-
const stats: Record<string, { pending: number; active: number }> = {};
|
|
1088
|
-
for (const [name, queue] of this.bulkheads) {
|
|
1089
|
-
stats[name] = {
|
|
1090
|
-
pending: queue.pending,
|
|
1091
|
-
active: queue.active,
|
|
1092
|
-
};
|
|
1093
|
-
}
|
|
1094
|
-
return {
|
|
1095
|
-
totalActive: this.totalActive,
|
|
1096
|
-
bulkheads: stats,
|
|
1097
|
-
};
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
// Usage: isolate failures by domain
|
|
1102
|
-
const bulkhead = new BulkheadPool(3, 30);
|
|
1103
|
-
|
|
1104
|
-
await Promise.all(
|
|
1105
|
-
urls.map(url => {
|
|
1106
|
-
const domain = new URL(url).hostname;
|
|
1107
|
-
return bulkhead.execute(domain, async () => {
|
|
1108
|
-
const html = await fetch(url).then(r => r.text());
|
|
1109
|
-
return parseHtml(html);
|
|
1110
|
-
});
|
|
1111
|
-
})
|
|
1112
|
-
);
|
|
1113
|
-
```
|
|
1114
|
-
|
|
1115
|
-
### Fallback Strategies
|
|
1116
|
-
|
|
1117
|
-
```typescript
|
|
1118
|
-
interface FallbackStrategy<T> {
|
|
1119
|
-
name: string;
|
|
1120
|
-
execute: () => Promise<T>;
|
|
1121
|
-
condition?: (error: Error) => boolean;
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
class FallbackChain<T> {
|
|
1125
|
-
private strategies: FallbackStrategy<T>[] = [];
|
|
1126
|
-
|
|
1127
|
-
add(strategy: FallbackStrategy<T>): this {
|
|
1128
|
-
this.strategies.push(strategy);
|
|
1129
|
-
return this;
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
async execute(): Promise<T> {
|
|
1133
|
-
let lastError: Error | undefined;
|
|
1134
|
-
|
|
1135
|
-
for (const strategy of this.strategies) {
|
|
1136
|
-
if (strategy.condition && lastError && !strategy.condition(lastError)) {
|
|
1137
|
-
continue;
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
try {
|
|
1141
|
-
return await strategy.execute();
|
|
1142
|
-
} catch (error) {
|
|
1143
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
1144
|
-
console.warn(`Fallback "${strategy.name}" failed: ${lastError.message}`);
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
throw lastError ?? new Error('All fallback strategies failed');
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
// Usage
|
|
1153
|
-
const fetchWithFallback = new FallbackChain<string>()
|
|
1154
|
-
.add({
|
|
1155
|
-
name: 'primary-fetch',
|
|
1156
|
-
execute: async () => {
|
|
1157
|
-
const res = await fetch(url);
|
|
1158
|
-
return res.text();
|
|
1159
|
-
},
|
|
1160
|
-
})
|
|
1161
|
-
.add({
|
|
1162
|
-
name: 'cache',
|
|
1163
|
-
condition: (err) => err.message.includes('fetch'),
|
|
1164
|
-
execute: async () => {
|
|
1165
|
-
const cached = await cache.get(url);
|
|
1166
|
-
if (!cached) throw new Error('No cache');
|
|
1167
|
-
return cached;
|
|
1168
|
-
},
|
|
1169
|
-
})
|
|
1170
|
-
.add({
|
|
1171
|
-
name: 'archive.org',
|
|
1172
|
-
condition: (err) => err.message.includes('404'),
|
|
1173
|
-
execute: async () => {
|
|
1174
|
-
const res = await fetch(`https://web.archive.org/web/2024/${url}`);
|
|
1175
|
-
return res.text();
|
|
1176
|
-
},
|
|
1177
|
-
})
|
|
1178
|
-
.add({
|
|
1179
|
-
name: 'default',
|
|
1180
|
-
execute: async () => '<html><body>Content unavailable</body></html>',
|
|
1181
|
-
});
|
|
1182
|
-
|
|
1183
|
-
const html = await fetchWithFallback.execute();
|
|
1184
|
-
```
|
|
1185
|
-
|
|
1186
|
-
---
|
|
1187
|
-
|
|
1188
|
-
## 7. Layered Resilience
|
|
1189
|
-
|
|
1190
|
-
### Retry + Circuit Breaker + Timeout Stack
|
|
1191
|
-
|
|
1192
|
-
```typescript
|
|
1193
|
-
import { CircuitBreaker, handle, retry, timeout } from 'cockatiel';
|
|
1194
|
-
|
|
1195
|
-
// Layer 1: Retry with exponential backoff
|
|
1196
|
-
const retryPolicy = retry({
|
|
1197
|
-
maxAttempts: 3,
|
|
1198
|
-
backoff: {
|
|
1199
|
-
initialDelay: 1000,
|
|
1200
|
-
maxDelay: 10_000,
|
|
1201
|
-
multiplier: 2,
|
|
1202
|
-
jitter: true,
|
|
1203
|
-
},
|
|
1204
|
-
handle: (error) => {
|
|
1205
|
-
// Only retry specific errors
|
|
1206
|
-
return error instanceof TypeError ||
|
|
1207
|
-
(error instanceof ScraperError && error.retryable);
|
|
1208
|
-
},
|
|
1209
|
-
});
|
|
1210
|
-
|
|
1211
|
-
// Layer 2: Circuit breaker
|
|
1212
|
-
const circuitBreaker = CircuitBreaker.byPercentage({
|
|
1213
|
-
threshold: 50,
|
|
1214
|
-
duration: 30_000,
|
|
1215
|
-
halfOpenAfter: 10_000,
|
|
1216
|
-
});
|
|
1217
|
-
|
|
1218
|
-
// Layer 3: Timeout
|
|
1219
|
-
const timeoutPolicy = timeout(30_000);
|
|
1220
|
-
|
|
1221
|
-
// Combine all layers
|
|
1222
|
-
async function resilientScrape(url: string): Promise<string> {
|
|
1223
|
-
// Stack: timeout → circuit breaker → retry → fetch
|
|
1224
|
-
const result = await handle(
|
|
1225
|
-
circuitBreaker,
|
|
1226
|
-
() =>
|
|
1227
|
-
retryPolicy.execute(async () => {
|
|
1228
|
-
const response = await timeoutPolicy.execute(async () => {
|
|
1229
|
-
const res = await fetch(url, {
|
|
1230
|
-
signal: AbortSignal.timeout(30_000),
|
|
1231
|
-
});
|
|
1232
|
-
return res;
|
|
1233
|
-
});
|
|
1234
|
-
return response.text();
|
|
1235
|
-
}),
|
|
1236
|
-
(error) => {
|
|
1237
|
-
// Circuit breaker fallback
|
|
1238
|
-
console.warn(`Circuit open, fallback for ${url}`);
|
|
1239
|
-
return getCachedVersion(url) ?? '';
|
|
1240
|
-
}
|
|
1241
|
-
).promise();
|
|
1242
|
-
|
|
1243
|
-
return result;
|
|
1244
|
-
}
|
|
1245
|
-
```
|
|
1246
|
-
|
|
1247
|
-
### Complete Resilient Scraper
|
|
1248
|
-
|
|
1249
|
-
```typescript
|
|
1250
|
-
import { CircuitBreaker, handle, retry, timeout, bulkhead } from 'cockatiel';
|
|
1251
|
-
import PQueue from 'p-queue';
|
|
1252
|
-
|
|
1253
|
-
interface ResilientScraperOptions {
|
|
1254
|
-
maxConcurrency: number;
|
|
1255
|
-
maxRetries: number;
|
|
1256
|
-
timeout: number;
|
|
1257
|
-
circuitBreakerThreshold: number;
|
|
1258
|
-
circuitBreakerResetTimeout: number;
|
|
1259
|
-
perDomainConcurrency: number;
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
class ResilientScraper {
|
|
1263
|
-
private queue: PQueue;
|
|
1264
|
-
private circuits = new Map<string, CircuitBreaker>();
|
|
1265
|
-
private options: ResilientScraperOptions;
|
|
1266
|
-
|
|
1267
|
-
constructor(options: Partial<ResilientScraperOptions> = {}) {
|
|
1268
|
-
this.options = {
|
|
1269
|
-
maxConcurrency: 10,
|
|
1270
|
-
maxRetries: 3,
|
|
1271
|
-
timeout: 30_000,
|
|
1272
|
-
circuitBreakerThreshold: 50,
|
|
1273
|
-
circuitBreakerResetTimeout: 30_000,
|
|
1274
|
-
perDomainConcurrency: 3,
|
|
1275
|
-
...options,
|
|
1276
|
-
};
|
|
1277
|
-
|
|
1278
|
-
this.queue = new PQueue({ concurrency: this.options.maxConcurrency });
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
private getCircuit(domain: string): CircuitBreaker {
|
|
1282
|
-
if (!this.circuits.has(domain)) {
|
|
1283
|
-
this.circuits.set(
|
|
1284
|
-
domain,
|
|
1285
|
-
CircuitBreaker.byPercentage({
|
|
1286
|
-
threshold: this.options.circuitBreakerThreshold,
|
|
1287
|
-
duration: this.options.circuitBreakerResetTimeout,
|
|
1288
|
-
halfOpenAfter: this.options.circuitBreakerResetTimeout / 3,
|
|
1289
|
-
})
|
|
1290
|
-
);
|
|
1291
|
-
}
|
|
1292
|
-
return this.circuits.get(domain)!;
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
async scrape(url: string): Promise<ScrapedItem> {
|
|
1296
|
-
const domain = new URL(url).hostname;
|
|
1297
|
-
const circuit = this.getCircuit(domain);
|
|
1298
|
-
|
|
1299
|
-
return this.queue.add(async () => {
|
|
1300
|
-
const result = await handle(
|
|
1301
|
-
circuit,
|
|
1302
|
-
() =>
|
|
1303
|
-
retry({
|
|
1304
|
-
maxAttempts: this.options.maxRetries,
|
|
1305
|
-
backoff: {
|
|
1306
|
-
initialDelay: 1000,
|
|
1307
|
-
maxDelay: 15_000,
|
|
1308
|
-
multiplier: 2,
|
|
1309
|
-
jitter: true,
|
|
1310
|
-
},
|
|
1311
|
-
}).execute(async () => {
|
|
1312
|
-
const response = await timeout(this.options.timeout).execute(async () => {
|
|
1313
|
-
const res = await fetch(url, {
|
|
1314
|
-
signal: AbortSignal.timeout(this.options.timeout),
|
|
1315
|
-
});
|
|
1316
|
-
if (!res.ok) {
|
|
1317
|
-
throw new HttpError(`HTTP ${res.status}`, res.status);
|
|
1318
|
-
}
|
|
1319
|
-
return res;
|
|
1320
|
-
});
|
|
1321
|
-
|
|
1322
|
-
const html = await response.text();
|
|
1323
|
-
return this.parseHtml(html, url);
|
|
1324
|
-
}),
|
|
1325
|
-
(error) => {
|
|
1326
|
-
console.warn(`Circuit open for ${domain}, using cache`);
|
|
1327
|
-
return this.getCachedResult(url);
|
|
1328
|
-
}
|
|
1329
|
-
).promise();
|
|
1330
|
-
|
|
1331
|
-
// Store in cache
|
|
1332
|
-
this.cacheResult(url, result);
|
|
1333
|
-
|
|
1334
|
-
return result;
|
|
1335
|
-
}) as Promise<ScrapedItem>;
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
async scrapeAll(urls: string[]): Promise<Map<string, ScrapedItem | null>> {
|
|
1339
|
-
const results = new Map<string, ScrapedItem | null>();
|
|
1340
|
-
|
|
1341
|
-
await Promise.all(
|
|
1342
|
-
urls.map(async (url) => {
|
|
1343
|
-
try {
|
|
1344
|
-
const result = await this.scrape(url);
|
|
1345
|
-
results.set(url, result);
|
|
1346
|
-
} catch (error) {
|
|
1347
|
-
console.error(`Failed: ${url}`, error);
|
|
1348
|
-
results.set(url, null);
|
|
1349
|
-
}
|
|
1350
|
-
})
|
|
1351
|
-
);
|
|
1352
|
-
|
|
1353
|
-
return results;
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
private async parseHtml(html: string, url: string): Promise<ScrapedItem> {
|
|
1357
|
-
// ... parsing logic
|
|
1358
|
-
return { url, title: '', content: html };
|
|
1359
|
-
}
|
|
1360
|
-
|
|
1361
|
-
private cacheResult(url: string, result: ScrapedItem): void {
|
|
1362
|
-
// Implement caching
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
private getCachedResult(url: string): ScrapedItem | null {
|
|
1366
|
-
// Return cached result or null
|
|
1367
|
-
return null;
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1370
|
-
```
|
|
1371
|
-
|
|
1372
|
-
---
|
|
1373
|
-
|
|
1374
|
-
## 8. Adaptive Concurrency
|
|
1375
|
-
|
|
1376
|
-
### Dynamic Concurrency Based on Error Rate
|
|
1377
|
-
|
|
1378
|
-
```typescript
|
|
1379
|
-
class AdaptiveConcurrency {
|
|
1380
|
-
private currentConcurrency: number;
|
|
1381
|
-
private minConcurrency: number;
|
|
1382
|
-
private maxConcurrency: number;
|
|
1383
|
-
private errorWindow: Array<{ timestamp: number; success: boolean }> = [];
|
|
1384
|
-
private windowSize: number = 100;
|
|
1385
|
-
private targetErrorRate: number = 0.1; // 10% target error rate
|
|
1386
|
-
private adjustmentInterval: ReturnType<typeof setInterval>;
|
|
1387
|
-
|
|
1388
|
-
constructor(options: {
|
|
1389
|
-
initialConcurrency?: number;
|
|
1390
|
-
minConcurrency?: number;
|
|
1391
|
-
maxConcurrency?: number;
|
|
1392
|
-
targetErrorRate?: number;
|
|
1393
|
-
adjustmentInterval?: number;
|
|
1394
|
-
} = {}) {
|
|
1395
|
-
this.currentConcurrency = options.initialConcurrency ?? 5;
|
|
1396
|
-
this.minConcurrency = options.minConcurrency ?? 1;
|
|
1397
|
-
this.maxConcurrency = options.maxConcurrency ?? 50;
|
|
1398
|
-
this.targetErrorRate = options.targetErrorRate ?? 0.1;
|
|
1399
|
-
|
|
1400
|
-
this.adjustmentInterval = setInterval(
|
|
1401
|
-
() => this.adjustConcurrency(),
|
|
1402
|
-
options.adjustmentInterval ?? 10_000
|
|
1403
|
-
);
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
track(success: boolean): void {
|
|
1407
|
-
this.errorWindow.push({
|
|
1408
|
-
timestamp: Date.now(),
|
|
1409
|
-
success,
|
|
1410
|
-
});
|
|
1411
|
-
|
|
1412
|
-
// Keep only recent results
|
|
1413
|
-
if (this.errorWindow.length > this.windowSize) {
|
|
1414
|
-
this.errorWindow.shift();
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
private adjustConcurrency(): void {
|
|
1419
|
-
if (this.errorWindow.length < 10) return; // Not enough data
|
|
1420
|
-
|
|
1421
|
-
const errorRate = this.getErrorRate();
|
|
1422
|
-
|
|
1423
|
-
if (errorRate > this.targetErrorRate * 1.5) {
|
|
1424
|
-
// Too many errors — reduce concurrency
|
|
1425
|
-
const reduction = Math.max(1, Math.floor(this.currentConcurrency * 0.3));
|
|
1426
|
-
this.currentConcurrency = Math.max(
|
|
1427
|
-
this.minConcurrency,
|
|
1428
|
-
this.currentConcurrency - reduction
|
|
1429
|
-
);
|
|
1430
|
-
console.log(`↓ Concurrency: ${this.currentConcurrency} (error rate: ${(errorRate * 100).toFixed(1)}%)`);
|
|
1431
|
-
} else if (errorRate < this.targetErrorRate * 0.5) {
|
|
1432
|
-
// Low errors — increase concurrency
|
|
1433
|
-
const increase = Math.max(1, Math.floor(this.currentConcurrency * 0.2));
|
|
1434
|
-
this.currentConcurrency = Math.min(
|
|
1435
|
-
this.maxConcurrency,
|
|
1436
|
-
this.currentConcurrency + increase
|
|
1437
|
-
);
|
|
1438
|
-
console.log(`↑ Concurrency: ${this.currentConcurrency} (error rate: ${(errorRate * 100).toFixed(1)}%)`);
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
private getErrorRate(): number {
|
|
1443
|
-
const errors = this.errorWindow.filter(e => !e.success).length;
|
|
1444
|
-
return errors / this.errorWindow.length;
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
get concurrency(): number {
|
|
1448
|
-
return this.currentConcurrency;
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1451
|
-
get errorRate(): number {
|
|
1452
|
-
return this.getErrorRate();
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
destroy(): void {
|
|
1456
|
-
clearInterval(this.adjustmentInterval);
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
// Usage
|
|
1461
|
-
const adaptive = new AdaptiveConcurrency({
|
|
1462
|
-
initialConcurrency: 5,
|
|
1463
|
-
minConcurrency: 1,
|
|
1464
|
-
maxConcurrency: 30,
|
|
1465
|
-
targetErrorRate: 0.1,
|
|
1466
|
-
});
|
|
1467
|
-
|
|
1468
|
-
const limit = pLimit(adaptive.concurrency);
|
|
1469
|
-
|
|
1470
|
-
// Dynamic concurrency — re-evaluate periodically
|
|
1471
|
-
async function scrapeWithAdaptiveConcurrency(urls: string[]): Promise<void> {
|
|
1472
|
-
const results: Promise<void>[] = [];
|
|
1473
|
-
|
|
1474
|
-
for (const url of urls) {
|
|
1475
|
-
const promise = limit(async () => {
|
|
1476
|
-
try {
|
|
1477
|
-
await scrapeUrl(url);
|
|
1478
|
-
adaptive.track(true);
|
|
1479
|
-
} catch (error) {
|
|
1480
|
-
adaptive.track(false);
|
|
1481
|
-
}
|
|
1482
|
-
});
|
|
1483
|
-
|
|
1484
|
-
results.push(promise);
|
|
1485
|
-
|
|
1486
|
-
// Re-create limiter when concurrency changes
|
|
1487
|
-
if (results.length % 10 === 0) {
|
|
1488
|
-
await Promise.race(results);
|
|
1489
|
-
// Update limiter's concurrency (p-limit doesn't support this,
|
|
1490
|
-
// so you'd need to recreate or use a different approach)
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
|
|
1494
|
-
await Promise.all(results);
|
|
1495
|
-
}
|
|
1496
|
-
```
|
|
1497
|
-
|
|
1498
|
-
### Performance-Based Adaptive Concurrency
|
|
1499
|
-
|
|
1500
|
-
```typescript
|
|
1501
|
-
class PerformanceAdaptiveConcurrency {
|
|
1502
|
-
private latencyWindow: number[] = [];
|
|
1503
|
-
private targetLatency: number;
|
|
1504
|
-
private windowSize: number = 50;
|
|
1505
|
-
private currentConcurrency: number;
|
|
1506
|
-
private minConcurrency: number;
|
|
1507
|
-
private maxConcurrency: number;
|
|
1508
|
-
|
|
1509
|
-
constructor(options: {
|
|
1510
|
-
initialConcurrency?: number;
|
|
1511
|
-
minConcurrency?: number;
|
|
1512
|
-
maxConcurrency?: number;
|
|
1513
|
-
targetLatency?: number; // ms
|
|
1514
|
-
} = {}) {
|
|
1515
|
-
this.currentConcurrency = options.initialConcurrency ?? 5;
|
|
1516
|
-
this.minConcurrency = options.minConcurrency ?? 1;
|
|
1517
|
-
this.maxConcurrency = options.maxConcurrency ?? 50;
|
|
1518
|
-
this.targetLatency = options.targetLatency ?? 2000;
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
trackLatency(latencyMs: number): void {
|
|
1522
|
-
this.latencyWindow.push(latencyMs);
|
|
1523
|
-
if (this.latencyWindow.length > this.windowSize) {
|
|
1524
|
-
this.latencyWindow.shift();
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
getOptimalConcurrency(): number {
|
|
1529
|
-
if (this.latencyWindow.length < 5) return this.currentConcurrency;
|
|
1530
|
-
|
|
1531
|
-
const avgLatency = this.latencyWindow.reduce((a, b) => a + b, 0)
|
|
1532
|
-
/ this.latencyWindow.length;
|
|
1533
|
-
|
|
1534
|
-
const p95Latency = this.getPercentile(95);
|
|
1535
|
-
const p99Latency = this.getPercentile(99);
|
|
1536
|
-
|
|
1537
|
-
// If p99 is 2x target, reduce concurrency
|
|
1538
|
-
if (p99Latency > this.targetLatency * 2) {
|
|
1539
|
-
this.currentConcurrency = Math.max(
|
|
1540
|
-
this.minConcurrency,
|
|
1541
|
-
Math.floor(this.currentConcurrency * 0.7)
|
|
1542
|
-
);
|
|
1543
|
-
}
|
|
1544
|
-
// If avg latency is below target, increase concurrency
|
|
1545
|
-
else if (avgLatency < this.targetLatency * 0.5) {
|
|
1546
|
-
this.currentConcurrency = Math.min(
|
|
1547
|
-
this.maxConcurrency,
|
|
1548
|
-
Math.ceil(this.currentConcurrency * 1.3)
|
|
1549
|
-
);
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1552
|
-
return this.currentConcurrency;
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
|
-
private getPercentile(percentile: number): number {
|
|
1556
|
-
const sorted = [...this.latencyWindow].sort((a, b) => a - b);
|
|
1557
|
-
const index = Math.ceil((percentile / 100) * sorted.length) - 1;
|
|
1558
|
-
return sorted[Math.max(0, index)];
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
get stats() {
|
|
1562
|
-
const sorted = [...this.latencyWindow].sort((a, b) => a - b);
|
|
1563
|
-
return {
|
|
1564
|
-
currentConcurrency: this.currentConcurrency,
|
|
1565
|
-
avgLatency: this.latencyWindow.length > 0
|
|
1566
|
-
? this.latencyWindow.reduce((a, b) => a + b, 0) / this.latencyWindow.length
|
|
1567
|
-
: 0,
|
|
1568
|
-
p50: this.getPercentile(50),
|
|
1569
|
-
p95: this.getPercentile(95),
|
|
1570
|
-
p99: this.getPercentile(99),
|
|
1571
|
-
sampleSize: this.latencyWindow.length,
|
|
1572
|
-
};
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
```
|
|
1576
|
-
|
|
1577
|
-
---
|
|
1578
|
-
|
|
1579
|
-
## 9. Complete Code Examples
|
|
1580
|
-
|
|
1581
|
-
### Full Batch Scraper with All Patterns
|
|
1582
|
-
|
|
1583
|
-
```typescript
|
|
1584
|
-
import PQueue from 'p-queue';
|
|
1585
|
-
import { CircuitBreaker, handle, retry, timeout } from 'cockatiel';
|
|
1586
|
-
import cliProgress from 'cli-progress';
|
|
1587
|
-
import { z } from 'zod';
|
|
1588
|
-
|
|
1589
|
-
// ============================================
|
|
1590
|
-
// Schema
|
|
1591
|
-
// ============================================
|
|
1592
|
-
|
|
1593
|
-
const BatchJobSchema = z.object({
|
|
1594
|
-
urls: z.array(z.string().url()).min(1).max(10_000),
|
|
1595
|
-
concurrency: z.number().int().min(1).max(100).default(10),
|
|
1596
|
-
maxRetries: z.number().int().min(0).max(10).default(3),
|
|
1597
|
-
timeout: z.number().int().min(1000).max(300_000).default(30_000),
|
|
1598
|
-
targetErrorRate: z.number().min(0).max(1).default(0.1),
|
|
1599
|
-
outputDir: z.string().default('./output'),
|
|
1600
|
-
});
|
|
1601
|
-
|
|
1602
|
-
type BatchJob = z.infer<typeof BatchJobSchema>;
|
|
1603
|
-
|
|
1604
|
-
// ============================================
|
|
1605
|
-
// Progress Tracker
|
|
1606
|
-
// ============================================
|
|
1607
|
-
|
|
1608
|
-
class ProgressTracker {
|
|
1609
|
-
private bar: cliProgress.SingleBar;
|
|
1610
|
-
private succeeded = 0;
|
|
1611
|
-
private failed = 0;
|
|
1612
|
-
private startTime: number;
|
|
1613
|
-
|
|
1614
|
-
constructor(total: number) {
|
|
1615
|
-
this.startTime = Date.now();
|
|
1616
|
-
this.bar = new cliProgress.SingleBar({
|
|
1617
|
-
format: 'Scraping |{bar}| {percentage}% | {value}/{total} | ✓ {succeeded} ✗ {failed} | {speed} url/s',
|
|
1618
|
-
barCompleteChar: '\u2588',
|
|
1619
|
-
barIncompleteChar: '\u2591',
|
|
1620
|
-
hideCursor: true,
|
|
1621
|
-
});
|
|
1622
|
-
this.bar.start(total, 0);
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
success(): void {
|
|
1626
|
-
this.succeeded++;
|
|
1627
|
-
this.update();
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
error(): void {
|
|
1631
|
-
this.failed++;
|
|
1632
|
-
this.update();
|
|
1633
|
-
}
|
|
1634
|
-
|
|
1635
|
-
private update(): void {
|
|
1636
|
-
const elapsed = (Date.now() - this.startTime) / 1000;
|
|
1637
|
-
const speed = ((this.succeeded + this.failed) / elapsed).toFixed(1);
|
|
1638
|
-
this.bar.update(this.succeeded + this.failed, {
|
|
1639
|
-
succeeded: this.succeeded,
|
|
1640
|
-
failed: this.failed,
|
|
1641
|
-
speed,
|
|
1642
|
-
});
|
|
1643
|
-
}
|
|
1644
|
-
|
|
1645
|
-
stop(): void {
|
|
1646
|
-
this.bar.stop();
|
|
1647
|
-
const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1);
|
|
1648
|
-
console.log(`\nCompleted in ${elapsed}s`);
|
|
1649
|
-
console.log(` ✓ Succeeded: ${this.succeeded}`);
|
|
1650
|
-
console.log(` ✗ Failed: ${this.failed}`);
|
|
1651
|
-
}
|
|
1652
|
-
}
|
|
1653
|
-
|
|
1654
|
-
// ============================================
|
|
1655
|
-
// Adaptive Concurrency Manager
|
|
1656
|
-
// ============================================
|
|
1657
|
-
|
|
1658
|
-
class AdaptiveConcurrencyManager {
|
|
1659
|
-
private errorWindow: boolean[] = [];
|
|
1660
|
-
private currentConcurrency: number;
|
|
1661
|
-
private minConcurrency: number;
|
|
1662
|
-
private maxConcurrency: number;
|
|
1663
|
-
private targetErrorRate: number;
|
|
1664
|
-
|
|
1665
|
-
constructor(options: {
|
|
1666
|
-
initial: number;
|
|
1667
|
-
min: number;
|
|
1668
|
-
max: number;
|
|
1669
|
-
targetErrorRate: number;
|
|
1670
|
-
}) {
|
|
1671
|
-
this.currentConcurrency = options.initial;
|
|
1672
|
-
this.minConcurrency = options.min;
|
|
1673
|
-
this.maxConcurrency = options.max;
|
|
1674
|
-
this.targetErrorRate = options.targetErrorRate;
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
track(success: boolean): void {
|
|
1678
|
-
this.errorWindow.push(success);
|
|
1679
|
-
if (this.errorWindow.length > 100) this.errorWindow.shift();
|
|
1680
|
-
this.adjust();
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
private adjust(): void {
|
|
1684
|
-
if (this.errorWindow.length < 20) return;
|
|
1685
|
-
|
|
1686
|
-
const errorRate = 1 - (this.errorWindow.filter(Boolean).length / this.errorWindow.length);
|
|
1687
|
-
|
|
1688
|
-
if (errorRate > this.targetErrorRate * 2) {
|
|
1689
|
-
this.currentConcurrency = Math.max(
|
|
1690
|
-
this.minConcurrency,
|
|
1691
|
-
Math.floor(this.currentConcurrency * 0.6)
|
|
1692
|
-
);
|
|
1693
|
-
} else if (errorRate < this.targetErrorRate * 0.3) {
|
|
1694
|
-
this.currentConcurrency = Math.min(
|
|
1695
|
-
this.maxConcurrency,
|
|
1696
|
-
Math.ceil(this.currentConcurrency * 1.4)
|
|
1697
|
-
);
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
get concurrency(): number {
|
|
1702
|
-
return this.currentConcurrency;
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1705
|
-
get errorRate(): number {
|
|
1706
|
-
if (this.errorWindow.length === 0) return 0;
|
|
1707
|
-
return 1 - (this.errorWindow.filter(Boolean).length / this.errorWindow.length);
|
|
1708
|
-
}
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
// ============================================
|
|
1712
|
-
// Circuit Breaker per Domain
|
|
1713
|
-
// ============================================
|
|
1714
|
-
|
|
1715
|
-
class CircuitBreakerPool {
|
|
1716
|
-
private circuits = new Map<string, CircuitBreaker>();
|
|
1717
|
-
|
|
1718
|
-
get(domain: string): CircuitBreaker {
|
|
1719
|
-
if (!this.circuits.has(domain)) {
|
|
1720
|
-
this.circuits.set(
|
|
1721
|
-
domain,
|
|
1722
|
-
CircuitBreaker.byPercentage({
|
|
1723
|
-
threshold: 50,
|
|
1724
|
-
duration: 30_000,
|
|
1725
|
-
halfOpenAfter: 10_000,
|
|
1726
|
-
})
|
|
1727
|
-
);
|
|
1728
|
-
}
|
|
1729
|
-
return this.circuits.get(domain)!;
|
|
1730
|
-
}
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
|
-
// ============================================
|
|
1734
|
-
// Main Batch Scraper
|
|
1735
|
-
// ============================================
|
|
1736
|
-
|
|
1737
|
-
class BatchScraper {
|
|
1738
|
-
private job: BatchJob;
|
|
1739
|
-
private adaptive: AdaptiveConcurrencyManager;
|
|
1740
|
-
private circuits: CircuitBreakerPool;
|
|
1741
|
-
private progress: ProgressTracker;
|
|
1742
|
-
private queue: PQueue;
|
|
1743
|
-
private results: Map<string, { success: boolean; data?: unknown; error?: string }> = new Map();
|
|
1744
|
-
|
|
1745
|
-
constructor(job: BatchJob) {
|
|
1746
|
-
this.job = job;
|
|
1747
|
-
this.adaptive = new AdaptiveConcurrencyManager({
|
|
1748
|
-
initial: job.concurrency,
|
|
1749
|
-
min: 1,
|
|
1750
|
-
max: job.concurrency * 2,
|
|
1751
|
-
targetErrorRate: job.targetErrorRate,
|
|
1752
|
-
});
|
|
1753
|
-
this.circuits = new CircuitBreakerPool();
|
|
1754
|
-
this.progress = new ProgressTracker(job.urls.length);
|
|
1755
|
-
this.queue = new PQueue({ concurrency: job.concurrency });
|
|
1756
|
-
}
|
|
1757
|
-
|
|
1758
|
-
async run(): Promise<Map<string, { success: boolean; data?: unknown; error?: string }>> {
|
|
1759
|
-
console.log(`Starting batch scrape of ${this.job.urls.length} URLs`);
|
|
1760
|
-
console.log(`Initial concurrency: ${this.job.concurrency}`);
|
|
1761
|
-
console.log(`Max retries: ${this.job.maxRetries}`);
|
|
1762
|
-
console.log('');
|
|
1763
|
-
|
|
1764
|
-
for (const url of this.job.urls) {
|
|
1765
|
-
this.queue.add(() => this.scrapeUrl(url));
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
|
-
await this.queue.onIdle();
|
|
1769
|
-
this.progress.stop();
|
|
1770
|
-
|
|
1771
|
-
// Print summary
|
|
1772
|
-
const succeeded = [...this.results.values()].filter(r => r.success).length;
|
|
1773
|
-
const failed = [...this.results.values()].filter(r => !r.success).length;
|
|
1774
|
-
console.log(`\nResults: ${succeeded} succeeded, ${failed} failed`);
|
|
1775
|
-
console.log(`Final concurrency: ${this.adaptive.concurrency}`);
|
|
1776
|
-
console.log(`Error rate: ${(this.adaptive.errorRate * 100).toFixed(1)}%`);
|
|
1777
|
-
|
|
1778
|
-
return this.results;
|
|
1779
|
-
}
|
|
1780
|
-
|
|
1781
|
-
private async scrapeUrl(url: string): Promise<void> {
|
|
1782
|
-
const domain = new URL(url).hostname;
|
|
1783
|
-
const circuit = this.circuits.get(domain);
|
|
1784
|
-
|
|
1785
|
-
try {
|
|
1786
|
-
const result = await handle(
|
|
1787
|
-
circuit,
|
|
1788
|
-
() =>
|
|
1789
|
-
retry({
|
|
1790
|
-
maxAttempts: this.job.maxRetries,
|
|
1791
|
-
backoff: {
|
|
1792
|
-
initialDelay: 1000,
|
|
1793
|
-
maxDelay: 15_000,
|
|
1794
|
-
multiplier: 2,
|
|
1795
|
-
jitter: true,
|
|
1796
|
-
},
|
|
1797
|
-
}).execute(async () => {
|
|
1798
|
-
const response = await timeout(this.job.timeout).execute(async () => {
|
|
1799
|
-
const res = await fetch(url, {
|
|
1800
|
-
headers: {
|
|
1801
|
-
'User-Agent': 'Mozilla/5.0 (compatible; BatchScraper/1.0)',
|
|
1802
|
-
},
|
|
1803
|
-
});
|
|
1804
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1805
|
-
return res;
|
|
1806
|
-
});
|
|
1807
|
-
|
|
1808
|
-
const html = await response.text();
|
|
1809
|
-
return this.parseHtml(html, url);
|
|
1810
|
-
}),
|
|
1811
|
-
() => {
|
|
1812
|
-
// Circuit breaker fallback
|
|
1813
|
-
return { cached: true, title: 'Content unavailable (circuit open)' };
|
|
1814
|
-
}
|
|
1815
|
-
).promise();
|
|
1816
|
-
|
|
1817
|
-
this.results.set(url, { success: true, data: result });
|
|
1818
|
-
this.adaptive.track(true);
|
|
1819
|
-
this.progress.success();
|
|
1820
|
-
} catch (error) {
|
|
1821
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1822
|
-
this.results.set(url, { success: false, error: errorMsg });
|
|
1823
|
-
this.adaptive.track(false);
|
|
1824
|
-
this.progress.error();
|
|
1825
|
-
}
|
|
1826
|
-
}
|
|
1827
|
-
|
|
1828
|
-
private async parseHtml(html: string, url: string): Promise<unknown> {
|
|
1829
|
-
// Simple extraction
|
|
1830
|
-
const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/i);
|
|
1831
|
-
return {
|
|
1832
|
-
url,
|
|
1833
|
-
title: titleMatch?.[1]?.trim() ?? '',
|
|
1834
|
-
length: html.length,
|
|
1835
|
-
scrapedAt: new Date().toISOString(),
|
|
1836
|
-
};
|
|
1837
|
-
}
|
|
1838
|
-
}
|
|
1839
|
-
|
|
1840
|
-
// ============================================
|
|
1841
|
-
// Usage
|
|
1842
|
-
// ============================================
|
|
1843
|
-
|
|
1844
|
-
async function main() {
|
|
1845
|
-
const job = BatchJobSchema.parse({
|
|
1846
|
-
urls: [
|
|
1847
|
-
'https://example.com/1',
|
|
1848
|
-
'https://example.com/2',
|
|
1849
|
-
'https://example.com/3',
|
|
1850
|
-
// ... more URLs
|
|
1851
|
-
],
|
|
1852
|
-
concurrency: 10,
|
|
1853
|
-
maxRetries: 3,
|
|
1854
|
-
timeout: 30_000,
|
|
1855
|
-
});
|
|
1856
|
-
|
|
1857
|
-
const scraper = new BatchScraper(job);
|
|
1858
|
-
const results = await scraper.run();
|
|
1859
|
-
|
|
1860
|
-
// Save results
|
|
1861
|
-
const fs = await import('fs/promises');
|
|
1862
|
-
const outputPath = `${job.outputDir}/results-${Date.now()}.json`;
|
|
1863
|
-
await fs.writeFile(outputPath, JSON.stringify([...results.entries()], null, 2));
|
|
1864
|
-
console.log(`\nResults saved to ${outputPath}`);
|
|
1865
|
-
}
|
|
1866
|
-
|
|
1867
|
-
main().catch(console.error);
|
|
1868
|
-
```
|
|
1869
|
-
|
|
1870
|
-
---
|
|
1871
|
-
|
|
1872
|
-
## Summary: Quick Reference
|
|
1873
|
-
|
|
1874
|
-
| Pattern | Package | Use Case |
|
|
1875
|
-
|---------|---------|----------|
|
|
1876
|
-
| Simple concurrency | p-limit | Wrap existing functions |
|
|
1877
|
-
| Map with concurrency | p-map | Batch process arrays |
|
|
1878
|
-
| Priority queue | p-queue | Dynamic task submission |
|
|
1879
|
-
| Terminal progress | cli-progress | Single progress bar |
|
|
1880
|
-
| Spinner | ora | Single operation feedback |
|
|
1881
|
-
| Browser pooling | Custom / Crawlee | Playwright/Puppeteer |
|
|
1882
|
-
| Autoscaled pool | Crawlee | Dynamic scaling |
|
|
1883
|
-
| Circuit breaker | cockatiel | Prevent cascading failures |
|
|
1884
|
-
| Retry + backoff | cockatiel | Transient error handling |
|
|
1885
|
-
| Timeout | cockatiel / AbortSignal | Prevent hanging |
|
|
1886
|
-
| Adaptive concurrency | Custom | Self-tuning systems |
|
|
1887
|
-
| Domain distribution | Custom | Per-domain rate limits |
|
|
1888
|
-
| Fallback chain | Custom | Graceful degradation |
|