github-to-mcp-monorepo 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +8 -0
- package/.github/CODEOWNERS +6 -0
- package/.husky/pre-commit +1 -0
- package/.nvmrc +1 -0
- package/.prettierignore +5 -0
- package/.prettierrc +7 -0
- package/.vscode/settings.json +4 -0
- package/ARCHITECTURE.md +1429 -0
- package/CHANGELOG.md +167 -0
- package/CONTRIBUTING.md +327 -0
- package/LICENSE +201 -0
- package/README.md +1028 -0
- package/SECURITY.md +248 -0
- package/VISUAL_GUIDE.md +437 -0
- package/apps/vscode/IMPLEMENTATION.md +480 -0
- package/apps/vscode/README.md +248 -0
- package/apps/vscode/package.json +381 -0
- package/apps/vscode/resources/icon.png +0 -0
- package/apps/vscode/resources/icon.svg +5 -0
- package/apps/vscode/src/commands/browseRegistry.ts +211 -0
- package/apps/vscode/src/commands/configureClaudeDesktop.ts +332 -0
- package/apps/vscode/src/commands/convert.ts +82 -0
- package/apps/vscode/src/commands/convertCurrentRepo.ts +109 -0
- package/apps/vscode/src/commands/convertFromUrl.ts +138 -0
- package/apps/vscode/src/commands/index.ts +121 -0
- package/apps/vscode/src/commands/validate.ts +197 -0
- package/apps/vscode/src/extension.ts +464 -0
- package/apps/vscode/src/global.d.ts +36 -0
- package/apps/vscode/src/test/extension.test.ts +73 -0
- package/apps/vscode/src/utils/file-generator.ts +529 -0
- package/apps/vscode/src/utils/github-api.ts +335 -0
- package/apps/vscode/src/utils/index.ts +29 -0
- package/apps/vscode/src/utils/mcp-config.ts +334 -0
- package/apps/vscode/src/utils/storage.ts +87 -0
- package/apps/vscode/src/views/McpServersTreeView.ts +160 -0
- package/apps/vscode/src/views/OutputChannelView.ts +195 -0
- package/apps/vscode/src/views/StatusBarItem.ts +251 -0
- package/apps/vscode/src/views/ToolsExplorerView.ts +314 -0
- package/apps/vscode/src/views/historyProvider.ts +75 -0
- package/apps/vscode/src/views/index.ts +12 -0
- package/apps/vscode/src/views/resultsPanel.ts +330 -0
- package/apps/vscode/src/webviews/ConversionPanel.ts +350 -0
- package/apps/vscode/src/webviews/ToolDetailsPanel.ts +448 -0
- package/apps/vscode/src/webviews/index.ts +9 -0
- package/apps/vscode/src/webviews/webview-ui/styles.ts +492 -0
- package/apps/vscode/tsconfig.json +20 -0
- package/apps/web/PLAYGROUND_GUIDE.md +499 -0
- package/apps/web/README.md +505 -0
- package/apps/web/app/api/convert/route.ts +100 -0
- package/apps/web/app/api/convert/stream/route.ts +198 -0
- package/apps/web/app/api/deploy/route.ts +157 -0
- package/apps/web/app/api/edge/route.ts +308 -0
- package/apps/web/app/api/export-docker/route.ts +284 -0
- package/apps/web/app/api/generate-openapi/route.ts +119 -0
- package/apps/web/app/api/mcp/[serverId]/route.ts +263 -0
- package/apps/web/app/api/playground/connect/route.ts +143 -0
- package/apps/web/app/api/playground/disconnect/route.ts +78 -0
- package/apps/web/app/api/playground/execute/route.ts +135 -0
- package/apps/web/app/api/playground/sessions/route.ts +103 -0
- package/apps/web/app/api/playground/tools/route.ts +117 -0
- package/apps/web/app/api/playground/v2/connect/route.ts +96 -0
- package/apps/web/app/api/playground/v2/disconnect/route.ts +88 -0
- package/apps/web/app/api/playground/v2/health/route.ts +80 -0
- package/apps/web/app/api/playground/v2/prompts/route.ts +160 -0
- package/apps/web/app/api/playground/v2/resources/route.ts +159 -0
- package/apps/web/app/api/playground/v2/sessions/route.ts +184 -0
- package/apps/web/app/api/playground/v2/tools/route.ts +167 -0
- package/apps/web/app/api/stream/route.ts +232 -0
- package/apps/web/app/batch/BatchConvertClient.tsx +190 -0
- package/apps/web/app/batch/page.tsx +37 -0
- package/apps/web/app/convert/page.tsx +269 -0
- package/apps/web/app/dashboard/page.tsx +380 -0
- package/apps/web/app/globals.css +622 -0
- package/apps/web/app/layout.tsx +120 -0
- package/apps/web/app/manifest.ts +31 -0
- package/apps/web/app/opengraph-image.tsx +112 -0
- package/apps/web/app/page.old.tsx +924 -0
- package/apps/web/app/page.tsx +77 -0
- package/apps/web/app/playground/page.tsx +306 -0
- package/apps/web/app/playground/v2/error.tsx +163 -0
- package/apps/web/app/playground/v2/layout.tsx +58 -0
- package/apps/web/app/playground/v2/loading.tsx +152 -0
- package/apps/web/app/playground/v2/page.tsx +644 -0
- package/apps/web/app/playground/v2/providers.tsx +214 -0
- package/apps/web/app/playground/v2/use-shortcuts.ts +209 -0
- package/apps/web/app/playground/v2/use-url-state.ts +296 -0
- package/apps/web/app/providers.tsx +22 -0
- package/apps/web/app/sitemap.ts +32 -0
- package/apps/web/app/twitter-image.tsx +112 -0
- package/apps/web/components/BranchSelector.tsx +401 -0
- package/apps/web/components/ClaudeConfigExport.tsx +226 -0
- package/apps/web/components/Features.tsx +84 -0
- package/apps/web/components/Footer.tsx +119 -0
- package/apps/web/components/GenerationProgress.tsx +248 -0
- package/apps/web/components/GithubUrlInput.tsx +483 -0
- package/apps/web/components/Header.tsx +175 -0
- package/apps/web/components/Hero.tsx +117 -0
- package/apps/web/components/HowItWorks.tsx +119 -0
- package/apps/web/components/InstallBanner.tsx +158 -0
- package/apps/web/components/Logo.tsx +116 -0
- package/apps/web/components/ParticleBackground.tsx +105 -0
- package/apps/web/components/Playground.tsx +472 -0
- package/apps/web/components/PlaygroundToolTester.tsx +410 -0
- package/apps/web/components/ProductCards.tsx +179 -0
- package/apps/web/components/SplitView.tsx +194 -0
- package/apps/web/components/ToolFilter.tsx +260 -0
- package/apps/web/components/ToolList.tsx +325 -0
- package/apps/web/components/batch/BatchConvert.tsx +785 -0
- package/apps/web/components/batch/index.ts +7 -0
- package/apps/web/components/convert/ConfigTabs.tsx +230 -0
- package/apps/web/components/convert/ConversionResult.tsx +482 -0
- package/apps/web/components/convert/InlinePlayground.tsx +259 -0
- package/apps/web/components/convert/LoadingSteps.tsx +311 -0
- package/apps/web/components/convert/OneClickInstall.tsx +224 -0
- package/apps/web/components/convert/ToolCard.tsx +189 -0
- package/apps/web/components/convert/TryInPlayground.tsx +242 -0
- package/apps/web/components/convert/index.ts +12 -0
- package/apps/web/components/deploy/DeployButton.tsx +369 -0
- package/apps/web/components/deploy/index.ts +7 -0
- package/apps/web/components/docker/DockerExport.tsx +690 -0
- package/apps/web/components/docker/index.ts +7 -0
- package/apps/web/components/install/OneClickInstall.tsx +676 -0
- package/apps/web/components/install/index.ts +7 -0
- package/apps/web/components/playground/CapabilityTabs.tsx +150 -0
- package/apps/web/components/playground/ConnectionStatusV2.tsx +322 -0
- package/apps/web/components/playground/EmptyStates.tsx +305 -0
- package/apps/web/components/playground/ExecutionLog.tsx +260 -0
- package/apps/web/components/playground/ExecutionLogV2.tsx +378 -0
- package/apps/web/components/playground/JsonViewer.tsx +388 -0
- package/apps/web/components/playground/PlaygroundLayout.tsx +244 -0
- package/apps/web/components/playground/PromptsPanel.tsx +385 -0
- package/apps/web/components/playground/ResourcesPanel.tsx +378 -0
- package/apps/web/components/playground/SchemaForm.tsx +477 -0
- package/apps/web/components/playground/ServerStatus.tsx +151 -0
- package/apps/web/components/playground/ShareButton.tsx +239 -0
- package/apps/web/components/playground/ToolsPanel.tsx +309 -0
- package/apps/web/components/playground/TransportConfigurator.tsx +563 -0
- package/apps/web/components/playground/index.ts +74 -0
- package/apps/web/components/playground/types.ts +202 -0
- package/apps/web/components/streaming/StreamingProgress.tsx +441 -0
- package/apps/web/components/streaming/index.ts +7 -0
- package/apps/web/components/ui/badge.tsx +42 -0
- package/apps/web/components/ui/button.tsx +88 -0
- package/apps/web/components/ui/card.tsx +75 -0
- package/apps/web/components/ui/code-block.tsx +122 -0
- package/apps/web/components/ui/index.ts +12 -0
- package/apps/web/components/ui/input.tsx +55 -0
- package/apps/web/components/ui/tabs.tsx +61 -0
- package/apps/web/hooks/index.ts +85 -0
- package/apps/web/hooks/types.ts +1173 -0
- package/apps/web/hooks/use-conversion.ts +133 -0
- package/apps/web/hooks/use-execution-history.ts +376 -0
- package/apps/web/hooks/use-generation-progress.ts +147 -0
- package/apps/web/hooks/use-local-storage.ts +88 -0
- package/apps/web/hooks/use-mcp-client.ts +623 -0
- package/apps/web/hooks/use-mcp-connection.ts +500 -0
- package/apps/web/hooks/use-mcp-execution.ts +282 -0
- package/apps/web/hooks/use-mcp-prompts.ts +441 -0
- package/apps/web/hooks/use-mcp-resources.ts +430 -0
- package/apps/web/hooks/use-mcp-tools.ts +540 -0
- package/apps/web/hooks/use-playground-store.ts +299 -0
- package/apps/web/hooks/use-playground.ts +184 -0
- package/apps/web/hooks/use-streaming-conversion.ts +227 -0
- package/apps/web/hooks/useBatchConversion.ts +271 -0
- package/apps/web/hooks/useDockerConfig.ts +161 -0
- package/apps/web/hooks/usePlatformDetection.ts +80 -0
- package/apps/web/hooks/useStreaming.ts +199 -0
- package/apps/web/lib/api/errors.ts +386 -0
- package/apps/web/lib/api/index.ts +137 -0
- package/apps/web/lib/api/logger.ts +187 -0
- package/apps/web/lib/api/middleware.ts +364 -0
- package/apps/web/lib/api/openapi.ts +977 -0
- package/apps/web/lib/api/session-manager.ts +594 -0
- package/apps/web/lib/api/types.ts +433 -0
- package/apps/web/lib/api/validation.ts +523 -0
- package/apps/web/lib/constants.ts +114 -0
- package/apps/web/lib/mcp/client.ts +1137 -0
- package/apps/web/lib/mcp/events.ts +651 -0
- package/apps/web/lib/mcp/index.ts +347 -0
- package/apps/web/lib/mcp/logger.ts +428 -0
- package/apps/web/lib/mcp/metrics.ts +703 -0
- package/apps/web/lib/mcp/retry.ts +616 -0
- package/apps/web/lib/mcp/session-manager.ts +779 -0
- package/apps/web/lib/mcp/transports.ts +988 -0
- package/apps/web/lib/mcp/types.ts +594 -0
- package/apps/web/lib/mcp-client-enhanced.ts +871 -0
- package/apps/web/lib/mcp-client.ts +778 -0
- package/apps/web/lib/mcp-errors.ts +489 -0
- package/apps/web/lib/mcp-sandbox.ts +593 -0
- package/apps/web/lib/mcp-testing.ts +428 -0
- package/apps/web/lib/mcp-types.ts +448 -0
- package/apps/web/lib/playground-store.tsx +1147 -0
- package/apps/web/lib/utils.ts +439 -0
- package/apps/web/next-env.d.ts +5 -0
- package/apps/web/next.config.js +23 -0
- package/apps/web/package.json +55 -0
- package/apps/web/postcss.config.js +6 -0
- package/apps/web/public/.well-known/ai-plugin.json +17 -0
- package/apps/web/public/logo.svg +6 -0
- package/apps/web/public/robots.txt +22 -0
- package/apps/web/public/schema.json +27 -0
- package/apps/web/tailwind.config.js +26 -0
- package/apps/web/tailwind.config.ts +123 -0
- package/apps/web/tsconfig.json +20 -0
- package/apps/web/types/deploy.ts +139 -0
- package/apps/web/types/index.ts +247 -0
- package/apps/web/vercel.json +39 -0
- package/eslint.config.mjs +23 -0
- package/llms.txt +102 -0
- package/mkdocs/docs/api/core.md +318 -0
- package/mkdocs/docs/api/index.md +128 -0
- package/mkdocs/docs/api/mcp-server.md +301 -0
- package/mkdocs/docs/api/openapi-parser.md +254 -0
- package/mkdocs/docs/assets/logo.svg +7 -0
- package/mkdocs/docs/changelog.md +118 -0
- package/mkdocs/docs/cli/generate.md +148 -0
- package/mkdocs/docs/cli/index.md +52 -0
- package/mkdocs/docs/cli/inspect.md +164 -0
- package/mkdocs/docs/cli/serve.md +136 -0
- package/mkdocs/docs/concepts/classification.md +254 -0
- package/mkdocs/docs/concepts/how-it-works.md +299 -0
- package/mkdocs/docs/concepts/index.md +77 -0
- package/mkdocs/docs/concepts/mcp-protocol.md +362 -0
- package/mkdocs/docs/concepts/tool-types.md +382 -0
- package/mkdocs/docs/contributing/architecture.md +262 -0
- package/mkdocs/docs/contributing/development.md +245 -0
- package/mkdocs/docs/contributing/index.md +73 -0
- package/mkdocs/docs/contributing/testing.md +320 -0
- package/mkdocs/docs/getting-started/configuration.md +235 -0
- package/mkdocs/docs/getting-started/index.md +54 -0
- package/mkdocs/docs/getting-started/installation.md +145 -0
- package/mkdocs/docs/getting-started/quickstart.md +160 -0
- package/mkdocs/docs/guides/batch.md +375 -0
- package/mkdocs/docs/guides/claude-desktop.md +227 -0
- package/mkdocs/docs/guides/cursor.md +188 -0
- package/mkdocs/docs/guides/custom-tools.md +367 -0
- package/mkdocs/docs/guides/index.md +78 -0
- package/mkdocs/docs/guides/private-repos.md +221 -0
- package/mkdocs/docs/guides/vscode.md +247 -0
- package/mkdocs/docs/index.md +175 -0
- package/mkdocs/docs/reference/config.md +223 -0
- package/mkdocs/docs/reference/env.md +192 -0
- package/mkdocs/docs/reference/index.md +102 -0
- package/mkdocs/docs/reference/tools.md +309 -0
- package/mkdocs/docs/stylesheets/extra.css +231 -0
- package/mkdocs/mkdocs.yml +204 -0
- package/mkdocs/overrides/.gitkeep +1 -0
- package/mkdocs/overrides/main.html +7 -0
- package/mkdocs/python-deps.txt +7 -0
- package/mkdocs/vercel.json +11 -0
- package/package.json +63 -0
- package/packages/core/package.json +61 -0
- package/packages/core/src/__tests__/bitbucket-client.test.ts +366 -0
- package/packages/core/src/__tests__/cli.test.ts +235 -0
- package/packages/core/src/__tests__/code-extractor.test.ts +378 -0
- package/packages/core/src/__tests__/docker-generator.test.ts +255 -0
- package/packages/core/src/__tests__/github-client.test.ts +390 -0
- package/packages/core/src/__tests__/gitlab-client.test.ts +319 -0
- package/packages/core/src/__tests__/go-extractor.test.ts +351 -0
- package/packages/core/src/__tests__/graphql-extractor.test.ts +330 -0
- package/packages/core/src/__tests__/java-extractor.test.ts +497 -0
- package/packages/core/src/__tests__/plugins.test.ts +467 -0
- package/packages/core/src/__tests__/readme-extractor.test.ts +258 -0
- package/packages/core/src/__tests__/redis-cache.test.ts +307 -0
- package/packages/core/src/__tests__/rust-extractor.test.ts +252 -0
- package/packages/core/src/__tests__/streaming.test.ts +251 -0
- package/packages/core/src/additional-extractors.ts +333 -0
- package/packages/core/src/cache/cache-interface.ts +179 -0
- package/packages/core/src/cache/index.ts +210 -0
- package/packages/core/src/cache/redis-cache.ts +291 -0
- package/packages/core/src/cache/upstash-cache.ts +379 -0
- package/packages/core/src/cache.ts +251 -0
- package/packages/core/src/cli.ts +822 -0
- package/packages/core/src/code-extractor.ts +696 -0
- package/packages/core/src/docker-generator.ts +470 -0
- package/packages/core/src/edge-compatible.ts +491 -0
- package/packages/core/src/extractors/go-extractor.ts +791 -0
- package/packages/core/src/extractors/index.ts +9 -0
- package/packages/core/src/extractors/java-extractor.ts +937 -0
- package/packages/core/src/extractors/rust-extractor.ts +744 -0
- package/packages/core/src/github-client.ts +319 -0
- package/packages/core/src/go-generator.ts +356 -0
- package/packages/core/src/graphql-extractor.ts +358 -0
- package/packages/core/src/index.ts +797 -0
- package/packages/core/src/langchain-exporter.ts +617 -0
- package/packages/core/src/language-parsers.ts +1114 -0
- package/packages/core/src/mcp-introspector.ts +279 -0
- package/packages/core/src/monorepo-detector.ts +378 -0
- package/packages/core/src/plugins/index.ts +370 -0
- package/packages/core/src/plugins/registry.ts +404 -0
- package/packages/core/src/plugins/types.ts +215 -0
- package/packages/core/src/providers/base-provider.ts +246 -0
- package/packages/core/src/providers/bitbucket-client.ts +464 -0
- package/packages/core/src/providers/gitlab-client.ts +388 -0
- package/packages/core/src/providers/index.ts +176 -0
- package/packages/core/src/python-generator.ts +260 -0
- package/packages/core/src/queue/index.ts +100 -0
- package/packages/core/src/queue/memory-queue.ts +445 -0
- package/packages/core/src/queue/redis-queue.ts +578 -0
- package/packages/core/src/queue/types.ts +251 -0
- package/packages/core/src/readme-extractor.ts +409 -0
- package/packages/core/src/schema-generator.ts +638 -0
- package/packages/core/src/streaming.ts +999 -0
- package/packages/core/src/types.ts +289 -0
- package/packages/core/tsconfig.json +9 -0
- package/packages/core/tsup.config.ts +25 -0
- package/packages/mcp-server/README.md +297 -0
- package/packages/mcp-server/package.json +55 -0
- package/packages/mcp-server/src/__tests__/mcp-server.test.ts +177 -0
- package/packages/mcp-server/src/__tests__/tools.test.ts +217 -0
- package/packages/mcp-server/src/index.ts +1206 -0
- package/packages/mcp-server/src/prompts/index.ts +601 -0
- package/packages/mcp-server/src/tools/export-docker.ts +362 -0
- package/packages/mcp-server/src/tools/generate-openapi.ts +162 -0
- package/packages/mcp-server/src/tools/monitor-mcp-server.ts +448 -0
- package/packages/mcp-server/src/tools/stream-convert.ts +398 -0
- package/packages/mcp-server/src/tools/test-mcp-tool.ts +531 -0
- package/packages/mcp-server/tsconfig.json +12 -0
- package/packages/mcp-server/tsup.config.ts +14 -0
- package/packages/openapi-parser/package-lock.json +3028 -0
- package/packages/openapi-parser/package.json +41 -0
- package/packages/openapi-parser/src/analyzer.ts +700 -0
- package/packages/openapi-parser/src/asyncapi-parser.ts +475 -0
- package/packages/openapi-parser/src/cli.ts +302 -0
- package/packages/openapi-parser/src/generator.ts +570 -0
- package/packages/openapi-parser/src/generators/express-analyzer.ts +649 -0
- package/packages/openapi-parser/src/generators/fastapi-analyzer.ts +960 -0
- package/packages/openapi-parser/src/generators/index.ts +200 -0
- package/packages/openapi-parser/src/generators/nextjs-analyzer.ts +768 -0
- package/packages/openapi-parser/src/generators/openapi-builder.ts +527 -0
- package/packages/openapi-parser/src/generators/types.ts +298 -0
- package/packages/openapi-parser/src/graphql-parser.ts +462 -0
- package/packages/openapi-parser/src/grpc-parser.ts +649 -0
- package/packages/openapi-parser/src/har-parser.ts +723 -0
- package/packages/openapi-parser/src/index.ts +635 -0
- package/packages/openapi-parser/src/insomnia-parser.ts +614 -0
- package/packages/openapi-parser/src/parser.ts +231 -0
- package/packages/openapi-parser/src/postman-parser.ts +611 -0
- package/packages/openapi-parser/src/ref-resolver.ts +313 -0
- package/packages/openapi-parser/src/transformer.ts +459 -0
- package/packages/openapi-parser/tests/generators/express.test.ts +209 -0
- package/packages/openapi-parser/tests/generators/fastapi.test.ts +236 -0
- package/packages/openapi-parser/tests/generators/nextjs.test.ts +273 -0
- package/packages/openapi-parser/tests/parsers.test.ts +847 -0
- package/packages/openapi-parser/tsconfig.json +9 -0
- package/packages/openapi-parser/tsup.config.ts +11 -0
- package/packages/registry/package.json +59 -0
- package/packages/registry/src/cli.ts +456 -0
- package/packages/registry/src/index.ts +44 -0
- package/packages/registry/src/popular/github.json +47 -0
- package/packages/registry/src/popular/index.ts +55 -0
- package/packages/registry/src/popular/linear.json +42 -0
- package/packages/registry/src/popular/notion.json +42 -0
- package/packages/registry/src/popular/openai.json +40 -0
- package/packages/registry/src/popular/resend.json +38 -0
- package/packages/registry/src/popular/slack.json +42 -0
- package/packages/registry/src/popular/stripe.json +163 -0
- package/packages/registry/src/popular/supabase.json +42 -0
- package/packages/registry/src/popular/twilio.json +40 -0
- package/packages/registry/src/popular/vercel.json +40 -0
- package/packages/registry/src/registry.ts +492 -0
- package/packages/registry/src/storage.ts +334 -0
- package/packages/registry/src/types.ts +275 -0
- package/packages/registry/src/updater.ts +208 -0
- package/packages/registry/tsconfig.json +10 -0
- package/packages/registry/tsup.config.ts +11 -0
- package/pnpm-workspace.yaml +3 -0
- package/scripts/build-docs.sh +16 -0
- package/server.json +9 -0
- package/templates/Dockerfile.python.template +60 -0
- package/templates/Dockerfile.typescript.template +60 -0
- package/templates/docker-compose.template.yml +68 -0
- package/tests/fixtures/express-app/index.js +34 -0
- package/tests/fixtures/express-app/routes/posts.js +43 -0
- package/tests/fixtures/express-app/routes/users.js +58 -0
- package/tests/fixtures/fastapi-app/main.py +125 -0
- package/tests/fixtures/fastapi-app/routes/admin.py +42 -0
- package/tests/fixtures/graphql/simple-schema.graphql +65 -0
- package/tests/fixtures/mocks/github-api-responses.json +63 -0
- package/tests/fixtures/nextjs-app/app/api/posts/route.ts +55 -0
- package/tests/fixtures/nextjs-app/app/api/users/[id]/route.ts +63 -0
- package/tests/fixtures/nextjs-app/app/api/users/route.ts +44 -0
- package/tests/fixtures/nextjs-app/pages/api/health.ts +28 -0
- package/tests/fixtures/openapi/petstore.yaml +179 -0
- package/tests/integration/langchain-export.test.ts +405 -0
- package/tests/integration/openapi-conversion.test.ts +221 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +32 -0
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Retry Utilities - Exponential backoff and circuit breaker patterns
|
|
3
|
+
* @copyright 2024-2026 nirholas
|
|
4
|
+
* @license MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Logger } from './logger.js';
|
|
8
|
+
import { NoopLogger } from './logger.js';
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Retry Configuration
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Retry strategy configuration
|
|
16
|
+
*/
|
|
17
|
+
export interface RetryConfig {
|
|
18
|
+
/** Maximum number of retry attempts (default: 3) */
|
|
19
|
+
readonly maxAttempts: number;
|
|
20
|
+
/** Initial delay in milliseconds (default: 1000) */
|
|
21
|
+
readonly initialDelayMs: number;
|
|
22
|
+
/** Maximum delay in milliseconds (default: 30000) */
|
|
23
|
+
readonly maxDelayMs: number;
|
|
24
|
+
/** Backoff multiplier (default: 2.0) */
|
|
25
|
+
readonly backoffMultiplier: number;
|
|
26
|
+
/** Whether to add jitter to delays (default: true) */
|
|
27
|
+
readonly jitter: boolean;
|
|
28
|
+
/** Jitter factor (0-1) (default: 0.25) */
|
|
29
|
+
readonly jitterFactor: number;
|
|
30
|
+
/** Timeout for each attempt in milliseconds (default: 30000) */
|
|
31
|
+
readonly attemptTimeoutMs: number;
|
|
32
|
+
/** Whether to retry on timeout errors (default: true) */
|
|
33
|
+
readonly retryOnTimeout: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Default retry configuration
|
|
38
|
+
*/
|
|
39
|
+
export const DEFAULT_RETRY_CONFIG: Readonly<RetryConfig> = {
|
|
40
|
+
maxAttempts: 3,
|
|
41
|
+
initialDelayMs: 1000,
|
|
42
|
+
maxDelayMs: 30_000,
|
|
43
|
+
backoffMultiplier: 2.0,
|
|
44
|
+
jitter: true,
|
|
45
|
+
jitterFactor: 0.25,
|
|
46
|
+
attemptTimeoutMs: 30_000,
|
|
47
|
+
retryOnTimeout: true,
|
|
48
|
+
} as const;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Partial retry configuration for customization
|
|
52
|
+
*/
|
|
53
|
+
export type RetryOptions = Partial<RetryConfig>;
|
|
54
|
+
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// Retry Context
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Context passed to retry callbacks
|
|
61
|
+
*/
|
|
62
|
+
export interface RetryContext {
|
|
63
|
+
/** Current attempt number (1-based) */
|
|
64
|
+
readonly attempt: number;
|
|
65
|
+
/** Total attempts allowed */
|
|
66
|
+
readonly maxAttempts: number;
|
|
67
|
+
/** Time since first attempt in ms */
|
|
68
|
+
readonly elapsedMs: number;
|
|
69
|
+
/** Previous error if any */
|
|
70
|
+
readonly lastError?: Error;
|
|
71
|
+
/** Whether this is the last attempt */
|
|
72
|
+
readonly isLastAttempt: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Retry result with attempt details
|
|
77
|
+
*/
|
|
78
|
+
export interface RetryResult<T> {
|
|
79
|
+
/** Whether the operation succeeded */
|
|
80
|
+
readonly success: boolean;
|
|
81
|
+
/** Result value if successful */
|
|
82
|
+
readonly value?: T;
|
|
83
|
+
/** Error if failed */
|
|
84
|
+
readonly error?: Error;
|
|
85
|
+
/** Number of attempts made */
|
|
86
|
+
readonly attempts: number;
|
|
87
|
+
/** Total time elapsed in ms */
|
|
88
|
+
readonly elapsedMs: number;
|
|
89
|
+
/** Whether retries were exhausted */
|
|
90
|
+
readonly retriesExhausted: boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Function type for retry predicate
|
|
95
|
+
*/
|
|
96
|
+
export type RetryPredicate = (error: Error, context: RetryContext) => boolean;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Function type for delay customization
|
|
100
|
+
*/
|
|
101
|
+
export type DelayFunction = (attempt: number, config: RetryConfig) => number;
|
|
102
|
+
|
|
103
|
+
// ============================================================================
|
|
104
|
+
// Retry Implementation
|
|
105
|
+
// ============================================================================
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Calculate delay with exponential backoff and optional jitter
|
|
109
|
+
*
|
|
110
|
+
* @param attempt - Current attempt number (1-based)
|
|
111
|
+
* @param config - Retry configuration
|
|
112
|
+
* @returns Delay in milliseconds
|
|
113
|
+
*/
|
|
114
|
+
export function calculateBackoffDelay(attempt: number, config: RetryConfig): number {
|
|
115
|
+
// Exponential backoff: initialDelay * multiplier^(attempt-1)
|
|
116
|
+
const exponentialDelay = config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt - 1);
|
|
117
|
+
|
|
118
|
+
// Clamp to maxDelay
|
|
119
|
+
let delay = Math.min(exponentialDelay, config.maxDelayMs);
|
|
120
|
+
|
|
121
|
+
// Add jitter if enabled
|
|
122
|
+
if (config.jitter) {
|
|
123
|
+
const jitterRange = delay * config.jitterFactor;
|
|
124
|
+
const jitter = (Math.random() - 0.5) * 2 * jitterRange;
|
|
125
|
+
delay = Math.max(0, delay + jitter);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return Math.round(delay);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Default retry predicate - retries on all errors except AbortError
|
|
133
|
+
*/
|
|
134
|
+
export const defaultRetryPredicate: RetryPredicate = (error: Error, context: RetryContext) => {
|
|
135
|
+
// Don't retry if this was the last attempt
|
|
136
|
+
if (context.isLastAttempt) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Don't retry abort errors
|
|
141
|
+
if (error.name === 'AbortError') {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Don't retry authentication errors
|
|
146
|
+
if (error.message.toLowerCase().includes('unauthorized') ||
|
|
147
|
+
error.message.toLowerCase().includes('forbidden') ||
|
|
148
|
+
error.message.toLowerCase().includes('authentication')) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Retry all other errors
|
|
153
|
+
return true;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Create a retry predicate that only retries specific error types
|
|
158
|
+
*/
|
|
159
|
+
export function createRetryPredicate(
|
|
160
|
+
retryableErrors: (new (...args: unknown[]) => Error)[]
|
|
161
|
+
): RetryPredicate {
|
|
162
|
+
return (error: Error, context: RetryContext) => {
|
|
163
|
+
if (context.isLastAttempt) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
return retryableErrors.some((ErrorType) => error instanceof ErrorType);
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Sleep for a specified duration
|
|
172
|
+
*/
|
|
173
|
+
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
174
|
+
return new Promise((resolve, reject) => {
|
|
175
|
+
if (signal?.aborted) {
|
|
176
|
+
reject(new DOMException('Aborted', 'AbortError'));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const timeoutId = setTimeout(resolve, ms);
|
|
181
|
+
|
|
182
|
+
signal?.addEventListener('abort', () => {
|
|
183
|
+
clearTimeout(timeoutId);
|
|
184
|
+
reject(new DOMException('Aborted', 'AbortError'));
|
|
185
|
+
}, { once: true });
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Execute an operation with retry logic
|
|
191
|
+
*
|
|
192
|
+
* @param operation - Async operation to execute
|
|
193
|
+
* @param options - Retry options
|
|
194
|
+
* @param shouldRetry - Optional custom retry predicate
|
|
195
|
+
* @param signal - Optional abort signal
|
|
196
|
+
* @param logger - Optional logger
|
|
197
|
+
* @returns Retry result
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* ```typescript
|
|
201
|
+
* const result = await retry(
|
|
202
|
+
* async () => {
|
|
203
|
+
* const response = await fetch('https://api.example.com/data');
|
|
204
|
+
* if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
205
|
+
* return response.json();
|
|
206
|
+
* },
|
|
207
|
+
* { maxAttempts: 5, initialDelayMs: 500 }
|
|
208
|
+
* );
|
|
209
|
+
*
|
|
210
|
+
* if (result.success) {
|
|
211
|
+
* console.log('Data:', result.value);
|
|
212
|
+
* } else {
|
|
213
|
+
* console.error('Failed after', result.attempts, 'attempts:', result.error);
|
|
214
|
+
* }
|
|
215
|
+
* ```
|
|
216
|
+
*/
|
|
217
|
+
export async function retry<T>(
|
|
218
|
+
operation: (context: RetryContext) => Promise<T>,
|
|
219
|
+
options: RetryOptions = {},
|
|
220
|
+
shouldRetry: RetryPredicate = defaultRetryPredicate,
|
|
221
|
+
signal?: AbortSignal,
|
|
222
|
+
logger: Logger = new NoopLogger()
|
|
223
|
+
): Promise<RetryResult<T>> {
|
|
224
|
+
const config: RetryConfig = { ...DEFAULT_RETRY_CONFIG, ...options };
|
|
225
|
+
const startTime = Date.now();
|
|
226
|
+
let lastError: Error | undefined;
|
|
227
|
+
|
|
228
|
+
for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
|
|
229
|
+
const context: RetryContext = {
|
|
230
|
+
attempt,
|
|
231
|
+
maxAttempts: config.maxAttempts,
|
|
232
|
+
elapsedMs: Date.now() - startTime,
|
|
233
|
+
lastError,
|
|
234
|
+
isLastAttempt: attempt === config.maxAttempts,
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
// Check abort signal
|
|
239
|
+
if (signal?.aborted) {
|
|
240
|
+
throw new DOMException('Operation aborted', 'AbortError');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
logger.debug(`Attempt ${attempt}/${config.maxAttempts}`, { data: { attempt } });
|
|
244
|
+
|
|
245
|
+
// Execute with timeout
|
|
246
|
+
const value = await executeWithTimeout(
|
|
247
|
+
() => operation(context),
|
|
248
|
+
config.attemptTimeoutMs,
|
|
249
|
+
signal
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
success: true,
|
|
254
|
+
value,
|
|
255
|
+
attempts: attempt,
|
|
256
|
+
elapsedMs: Date.now() - startTime,
|
|
257
|
+
retriesExhausted: false,
|
|
258
|
+
};
|
|
259
|
+
} catch (error) {
|
|
260
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
261
|
+
|
|
262
|
+
// Update context with error
|
|
263
|
+
const errorContext: RetryContext = {
|
|
264
|
+
...context,
|
|
265
|
+
lastError,
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
logger.debug(`Attempt ${attempt} failed: ${lastError.message}`, {
|
|
269
|
+
data: { attempt, error: lastError.message },
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Check if we should retry
|
|
273
|
+
if (!shouldRetry(lastError, errorContext)) {
|
|
274
|
+
logger.debug('Not retrying - predicate returned false');
|
|
275
|
+
return {
|
|
276
|
+
success: false,
|
|
277
|
+
error: lastError,
|
|
278
|
+
attempts: attempt,
|
|
279
|
+
elapsedMs: Date.now() - startTime,
|
|
280
|
+
retriesExhausted: false,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Don't delay after the last attempt
|
|
285
|
+
if (attempt < config.maxAttempts) {
|
|
286
|
+
const delay = calculateBackoffDelay(attempt, config);
|
|
287
|
+
logger.debug(`Waiting ${delay}ms before retry`, { data: { delay } });
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
await sleep(delay, signal);
|
|
291
|
+
} catch (sleepError) {
|
|
292
|
+
// Abort signal was triggered during sleep
|
|
293
|
+
return {
|
|
294
|
+
success: false,
|
|
295
|
+
error: sleepError instanceof Error ? sleepError : new Error(String(sleepError)),
|
|
296
|
+
attempts: attempt,
|
|
297
|
+
elapsedMs: Date.now() - startTime,
|
|
298
|
+
retriesExhausted: false,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// All retries exhausted
|
|
306
|
+
return {
|
|
307
|
+
success: false,
|
|
308
|
+
error: lastError ?? new Error('Unknown error'),
|
|
309
|
+
attempts: config.maxAttempts,
|
|
310
|
+
elapsedMs: Date.now() - startTime,
|
|
311
|
+
retriesExhausted: true,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Execute an operation with timeout
|
|
317
|
+
*/
|
|
318
|
+
async function executeWithTimeout<T>(
|
|
319
|
+
operation: () => Promise<T>,
|
|
320
|
+
timeoutMs: number,
|
|
321
|
+
signal?: AbortSignal
|
|
322
|
+
): Promise<T> {
|
|
323
|
+
// Create timeout promise
|
|
324
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
325
|
+
const timeoutId = setTimeout(() => {
|
|
326
|
+
reject(new Error(`Operation timed out after ${timeoutMs}ms`));
|
|
327
|
+
}, timeoutMs);
|
|
328
|
+
|
|
329
|
+
// Clean up timeout if signal is aborted
|
|
330
|
+
signal?.addEventListener('abort', () => clearTimeout(timeoutId), { once: true });
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
return Promise.race([operation(), timeoutPromise]);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ============================================================================
|
|
337
|
+
// Circuit Breaker
|
|
338
|
+
// ============================================================================
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Circuit breaker state
|
|
342
|
+
*/
|
|
343
|
+
export type CircuitState = 'closed' | 'open' | 'half-open';
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Circuit breaker configuration
|
|
347
|
+
*/
|
|
348
|
+
export interface CircuitBreakerConfig {
|
|
349
|
+
/** Number of failures before opening circuit (default: 5) */
|
|
350
|
+
readonly failureThreshold: number;
|
|
351
|
+
/** Time in ms before attempting to close circuit (default: 60000) */
|
|
352
|
+
readonly resetTimeoutMs: number;
|
|
353
|
+
/** Number of successes needed to close circuit from half-open (default: 2) */
|
|
354
|
+
readonly successThreshold: number;
|
|
355
|
+
/** Timeout for operations in ms (default: 30000) */
|
|
356
|
+
readonly operationTimeoutMs: number;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Default circuit breaker configuration
|
|
361
|
+
*/
|
|
362
|
+
export const DEFAULT_CIRCUIT_BREAKER_CONFIG: Readonly<CircuitBreakerConfig> = {
|
|
363
|
+
failureThreshold: 5,
|
|
364
|
+
resetTimeoutMs: 60_000,
|
|
365
|
+
successThreshold: 2,
|
|
366
|
+
operationTimeoutMs: 30_000,
|
|
367
|
+
} as const;
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Circuit breaker statistics
|
|
371
|
+
*/
|
|
372
|
+
export interface CircuitBreakerStats {
|
|
373
|
+
readonly state: CircuitState;
|
|
374
|
+
readonly failures: number;
|
|
375
|
+
readonly successes: number;
|
|
376
|
+
readonly lastFailure?: Date;
|
|
377
|
+
readonly lastSuccess?: Date;
|
|
378
|
+
readonly lastStateChange: Date;
|
|
379
|
+
readonly totalRequests: number;
|
|
380
|
+
readonly totalFailures: number;
|
|
381
|
+
readonly totalSuccesses: number;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Circuit breaker error thrown when circuit is open
|
|
386
|
+
*/
|
|
387
|
+
export class CircuitOpenError extends Error {
|
|
388
|
+
readonly name = 'CircuitOpenError';
|
|
389
|
+
readonly resetTime: Date;
|
|
390
|
+
|
|
391
|
+
constructor(resetTime: Date) {
|
|
392
|
+
super(`Circuit is open. Will attempt reset at ${resetTime.toISOString()}`);
|
|
393
|
+
this.resetTime = resetTime;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Circuit breaker for preventing cascading failures
|
|
399
|
+
*
|
|
400
|
+
* @example
|
|
401
|
+
* ```typescript
|
|
402
|
+
* const breaker = new CircuitBreaker({ failureThreshold: 3 });
|
|
403
|
+
*
|
|
404
|
+
* try {
|
|
405
|
+
* const result = await breaker.execute(async () => {
|
|
406
|
+
* return await riskyOperation();
|
|
407
|
+
* });
|
|
408
|
+
* } catch (error) {
|
|
409
|
+
* if (error instanceof CircuitOpenError) {
|
|
410
|
+
* console.log('Circuit is open, try again at', error.resetTime);
|
|
411
|
+
* }
|
|
412
|
+
* }
|
|
413
|
+
* ```
|
|
414
|
+
*/
|
|
415
|
+
export class CircuitBreaker {
|
|
416
|
+
private readonly _config: CircuitBreakerConfig;
|
|
417
|
+
private readonly _logger: Logger;
|
|
418
|
+
private _state: CircuitState = 'closed';
|
|
419
|
+
private _failures = 0;
|
|
420
|
+
private _successes = 0;
|
|
421
|
+
private _lastFailure?: Date;
|
|
422
|
+
private _lastSuccess?: Date;
|
|
423
|
+
private _lastStateChange = new Date();
|
|
424
|
+
private _totalRequests = 0;
|
|
425
|
+
private _totalFailures = 0;
|
|
426
|
+
private _totalSuccesses = 0;
|
|
427
|
+
private _resetTimer?: ReturnType<typeof setTimeout>;
|
|
428
|
+
|
|
429
|
+
constructor(config: Partial<CircuitBreakerConfig> = {}, logger: Logger = new NoopLogger()) {
|
|
430
|
+
this._config = { ...DEFAULT_CIRCUIT_BREAKER_CONFIG, ...config };
|
|
431
|
+
this._logger = logger;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Current circuit state
|
|
436
|
+
*/
|
|
437
|
+
get state(): CircuitState {
|
|
438
|
+
return this._state;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Get circuit breaker statistics
|
|
443
|
+
*/
|
|
444
|
+
get stats(): CircuitBreakerStats {
|
|
445
|
+
return {
|
|
446
|
+
state: this._state,
|
|
447
|
+
failures: this._failures,
|
|
448
|
+
successes: this._successes,
|
|
449
|
+
lastFailure: this._lastFailure,
|
|
450
|
+
lastSuccess: this._lastSuccess,
|
|
451
|
+
lastStateChange: this._lastStateChange,
|
|
452
|
+
totalRequests: this._totalRequests,
|
|
453
|
+
totalFailures: this._totalFailures,
|
|
454
|
+
totalSuccesses: this._totalSuccesses,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Execute an operation through the circuit breaker
|
|
460
|
+
*
|
|
461
|
+
* @param operation - Operation to execute
|
|
462
|
+
* @returns Operation result
|
|
463
|
+
* @throws {CircuitOpenError} If circuit is open
|
|
464
|
+
*/
|
|
465
|
+
async execute<T>(operation: () => Promise<T>): Promise<T> {
|
|
466
|
+
this._totalRequests++;
|
|
467
|
+
|
|
468
|
+
// Check if circuit is open
|
|
469
|
+
if (this._state === 'open') {
|
|
470
|
+
const resetTime = new Date(this._lastStateChange.getTime() + this._config.resetTimeoutMs);
|
|
471
|
+
const now = new Date();
|
|
472
|
+
|
|
473
|
+
if (now < resetTime) {
|
|
474
|
+
throw new CircuitOpenError(resetTime);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Transition to half-open
|
|
478
|
+
this._transitionTo('half-open');
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
try {
|
|
482
|
+
// Execute with timeout
|
|
483
|
+
const result = await executeWithTimeout(operation, this._config.operationTimeoutMs);
|
|
484
|
+
this._recordSuccess();
|
|
485
|
+
return result;
|
|
486
|
+
} catch (error) {
|
|
487
|
+
this._recordFailure();
|
|
488
|
+
throw error;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Manually reset the circuit breaker
|
|
494
|
+
*/
|
|
495
|
+
reset(): void {
|
|
496
|
+
this._failures = 0;
|
|
497
|
+
this._successes = 0;
|
|
498
|
+
this._transitionTo('closed');
|
|
499
|
+
this._clearResetTimer();
|
|
500
|
+
this._logger.info('Circuit breaker manually reset');
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Force the circuit to open
|
|
505
|
+
*/
|
|
506
|
+
forceOpen(): void {
|
|
507
|
+
this._transitionTo('open');
|
|
508
|
+
this._logger.warn('Circuit breaker forced open');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Dispose of the circuit breaker
|
|
513
|
+
*/
|
|
514
|
+
dispose(): void {
|
|
515
|
+
this._clearResetTimer();
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
private _recordSuccess(): void {
|
|
519
|
+
this._successes++;
|
|
520
|
+
this._totalSuccesses++;
|
|
521
|
+
this._lastSuccess = new Date();
|
|
522
|
+
this._failures = 0; // Reset failure count on success
|
|
523
|
+
|
|
524
|
+
if (this._state === 'half-open') {
|
|
525
|
+
if (this._successes >= this._config.successThreshold) {
|
|
526
|
+
this._transitionTo('closed');
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
private _recordFailure(): void {
|
|
532
|
+
this._failures++;
|
|
533
|
+
this._totalFailures++;
|
|
534
|
+
this._lastFailure = new Date();
|
|
535
|
+
this._successes = 0; // Reset success count on failure
|
|
536
|
+
|
|
537
|
+
if (this._state === 'closed') {
|
|
538
|
+
if (this._failures >= this._config.failureThreshold) {
|
|
539
|
+
this._transitionTo('open');
|
|
540
|
+
this._scheduleReset();
|
|
541
|
+
}
|
|
542
|
+
} else if (this._state === 'half-open') {
|
|
543
|
+
// Any failure in half-open state opens the circuit
|
|
544
|
+
this._transitionTo('open');
|
|
545
|
+
this._scheduleReset();
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
private _transitionTo(newState: CircuitState): void {
|
|
550
|
+
const previousState = this._state;
|
|
551
|
+
this._state = newState;
|
|
552
|
+
this._lastStateChange = new Date();
|
|
553
|
+
this._logger.info(`Circuit breaker: ${previousState} -> ${newState}`);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
private _scheduleReset(): void {
|
|
557
|
+
this._clearResetTimer();
|
|
558
|
+
this._resetTimer = setTimeout(() => {
|
|
559
|
+
if (this._state === 'open') {
|
|
560
|
+
this._transitionTo('half-open');
|
|
561
|
+
}
|
|
562
|
+
}, this._config.resetTimeoutMs);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
private _clearResetTimer(): void {
|
|
566
|
+
if (this._resetTimer) {
|
|
567
|
+
clearTimeout(this._resetTimer);
|
|
568
|
+
this._resetTimer = undefined;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// ============================================================================
|
|
574
|
+
// Retry with Circuit Breaker
|
|
575
|
+
// ============================================================================
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Combined retry and circuit breaker options
|
|
579
|
+
*/
|
|
580
|
+
export interface RetryWithCircuitBreakerOptions {
|
|
581
|
+
readonly retry?: RetryOptions;
|
|
582
|
+
readonly circuitBreaker?: Partial<CircuitBreakerConfig>;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Execute an operation with both retry logic and circuit breaker protection
|
|
587
|
+
*
|
|
588
|
+
* @param operation - Operation to execute
|
|
589
|
+
* @param options - Combined options
|
|
590
|
+
* @param signal - Optional abort signal
|
|
591
|
+
* @param logger - Optional logger
|
|
592
|
+
* @returns Retry result
|
|
593
|
+
*/
|
|
594
|
+
export async function retryWithCircuitBreaker<T>(
|
|
595
|
+
operation: (context: RetryContext) => Promise<T>,
|
|
596
|
+
circuitBreaker: CircuitBreaker,
|
|
597
|
+
options: RetryOptions = {},
|
|
598
|
+
signal?: AbortSignal,
|
|
599
|
+
logger: Logger = new NoopLogger()
|
|
600
|
+
): Promise<RetryResult<T>> {
|
|
601
|
+
return retry(
|
|
602
|
+
async (context) => {
|
|
603
|
+
return circuitBreaker.execute(() => operation(context));
|
|
604
|
+
},
|
|
605
|
+
options,
|
|
606
|
+
(error, context) => {
|
|
607
|
+
// Don't retry circuit open errors
|
|
608
|
+
if (error instanceof CircuitOpenError) {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
return defaultRetryPredicate(error, context);
|
|
612
|
+
},
|
|
613
|
+
signal,
|
|
614
|
+
logger
|
|
615
|
+
);
|
|
616
|
+
}
|