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,379 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Upstash Redis cache implementation for serverless environments
|
|
3
|
+
* @copyright Copyright (c) 2024-2026 nirholas
|
|
4
|
+
* @license MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
CacheInterface,
|
|
9
|
+
CacheEntry,
|
|
10
|
+
CacheStats,
|
|
11
|
+
CacheConfig,
|
|
12
|
+
CacheSerializer
|
|
13
|
+
} from './cache-interface';
|
|
14
|
+
import { defaultSerializer } from './cache-interface';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Upstash REST API response
|
|
18
|
+
*/
|
|
19
|
+
interface UpstashResponse<T = unknown> {
|
|
20
|
+
result: T;
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Upstash cache configuration
|
|
26
|
+
*/
|
|
27
|
+
export interface UpstashCacheConfig extends CacheConfig {
|
|
28
|
+
/** Upstash REST URL */
|
|
29
|
+
url: string;
|
|
30
|
+
/** Upstash REST token */
|
|
31
|
+
token: string;
|
|
32
|
+
/** Key prefix for namespacing */
|
|
33
|
+
keyPrefix?: string;
|
|
34
|
+
/** Request timeout in milliseconds */
|
|
35
|
+
timeout?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Upstash Redis cache implementation
|
|
40
|
+
* Uses REST API for serverless/edge compatibility
|
|
41
|
+
*/
|
|
42
|
+
export class UpstashCache implements CacheInterface {
|
|
43
|
+
private url: string;
|
|
44
|
+
private token: string;
|
|
45
|
+
private config: UpstashCacheConfig;
|
|
46
|
+
private serializer: CacheSerializer;
|
|
47
|
+
private stats: { hits: number; misses: number };
|
|
48
|
+
|
|
49
|
+
constructor(config: UpstashCacheConfig) {
|
|
50
|
+
this.url = config.url.replace(/\/$/, ''); // Remove trailing slash
|
|
51
|
+
this.token = config.token;
|
|
52
|
+
this.config = {
|
|
53
|
+
keyPrefix: 'mcp-cache',
|
|
54
|
+
defaultTTL: 3600,
|
|
55
|
+
timeout: 5000,
|
|
56
|
+
...config
|
|
57
|
+
};
|
|
58
|
+
this.serializer = config.serializer || defaultSerializer;
|
|
59
|
+
this.stats = { hits: 0, misses: 0 };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Generate prefixed key
|
|
64
|
+
*/
|
|
65
|
+
private prefixKey(key: string): string {
|
|
66
|
+
return this.config.keyPrefix ? `${this.config.keyPrefix}:${key}` : key;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Execute Upstash command via REST API
|
|
71
|
+
*/
|
|
72
|
+
private async execute<T>(command: string[]): Promise<T> {
|
|
73
|
+
const controller = new AbortController();
|
|
74
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const response = await fetch(`${this.url}`, {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: {
|
|
80
|
+
Authorization: `Bearer ${this.token}`,
|
|
81
|
+
'Content-Type': 'application/json'
|
|
82
|
+
},
|
|
83
|
+
body: JSON.stringify(command),
|
|
84
|
+
signal: controller.signal
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
throw new Error(`Upstash API error: ${response.status} ${response.statusText}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const data = await response.json() as UpstashResponse<T>;
|
|
92
|
+
|
|
93
|
+
if (data.error) {
|
|
94
|
+
throw new Error(`Upstash error: ${data.error}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return data.result;
|
|
98
|
+
} finally {
|
|
99
|
+
clearTimeout(timeoutId);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Execute pipeline of commands
|
|
105
|
+
*/
|
|
106
|
+
private async pipeline<T>(commands: string[][]): Promise<T[]> {
|
|
107
|
+
const controller = new AbortController();
|
|
108
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const response = await fetch(`${this.url}/pipeline`, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: {
|
|
114
|
+
Authorization: `Bearer ${this.token}`,
|
|
115
|
+
'Content-Type': 'application/json'
|
|
116
|
+
},
|
|
117
|
+
body: JSON.stringify(commands),
|
|
118
|
+
signal: controller.signal
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
if (!response.ok) {
|
|
122
|
+
throw new Error(`Upstash API error: ${response.status} ${response.statusText}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const data = await response.json() as UpstashResponse<T>[];
|
|
126
|
+
return data.map(d => d.result);
|
|
127
|
+
} finally {
|
|
128
|
+
clearTimeout(timeoutId);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get a cached value
|
|
134
|
+
*/
|
|
135
|
+
async get<T>(key: string): Promise<CacheEntry<T> | null> {
|
|
136
|
+
const prefixedKey = this.prefixKey(key);
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const value = await this.execute<string | null>(['GET', prefixedKey]);
|
|
140
|
+
|
|
141
|
+
if (!value) {
|
|
142
|
+
this.stats.misses++;
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const entry = this.serializer.deserialize<CacheEntry<T>>(value);
|
|
147
|
+
|
|
148
|
+
// Check if entry is expired
|
|
149
|
+
if (Date.now() - entry.timestamp > entry.ttl * 1000) {
|
|
150
|
+
this.stats.misses++;
|
|
151
|
+
// Delete expired entry asynchronously
|
|
152
|
+
this.delete(key).catch(() => {});
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
this.stats.hits++;
|
|
157
|
+
return entry;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
this.stats.misses++;
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Set a cached value
|
|
166
|
+
*/
|
|
167
|
+
async set<T>(key: string, entry: CacheEntry<T>): Promise<void> {
|
|
168
|
+
const prefixedKey = this.prefixKey(key);
|
|
169
|
+
const serialized = this.serializer.serialize(entry);
|
|
170
|
+
|
|
171
|
+
// Calculate expiration time
|
|
172
|
+
const expirationSeconds = this.config.staleWhileRevalidate
|
|
173
|
+
? Math.ceil(entry.ttl * (this.config.staleTolerance || 2))
|
|
174
|
+
: entry.ttl;
|
|
175
|
+
|
|
176
|
+
if (expirationSeconds > 0) {
|
|
177
|
+
await this.execute(['SET', prefixedKey, serialized, 'EX', String(expirationSeconds)]);
|
|
178
|
+
} else {
|
|
179
|
+
await this.execute(['SET', prefixedKey, serialized]);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Delete a cached value
|
|
185
|
+
*/
|
|
186
|
+
async delete(key: string): Promise<void> {
|
|
187
|
+
const prefixedKey = this.prefixKey(key);
|
|
188
|
+
await this.execute(['DEL', prefixedKey]);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Clear all cached values with the prefix
|
|
193
|
+
*/
|
|
194
|
+
async clear(): Promise<void> {
|
|
195
|
+
const pattern = this.config.keyPrefix ? `${this.config.keyPrefix}:*` : '*';
|
|
196
|
+
|
|
197
|
+
// Get all keys matching the pattern
|
|
198
|
+
const keys = await this.execute<string[]>(['KEYS', pattern]);
|
|
199
|
+
|
|
200
|
+
if (keys && keys.length > 0) {
|
|
201
|
+
// Delete in batches
|
|
202
|
+
const batchSize = 100;
|
|
203
|
+
for (let i = 0; i < keys.length; i += batchSize) {
|
|
204
|
+
const batch = keys.slice(i, i + batchSize);
|
|
205
|
+
await this.execute(['DEL', ...batch]);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Reset stats
|
|
210
|
+
this.stats = { hits: 0, misses: 0 };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Check if a key exists
|
|
215
|
+
*/
|
|
216
|
+
async has(key: string): Promise<boolean> {
|
|
217
|
+
const prefixedKey = this.prefixKey(key);
|
|
218
|
+
const exists = await this.execute<number>(['EXISTS', prefixedKey]);
|
|
219
|
+
return exists > 0;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Get multiple values at once
|
|
224
|
+
*/
|
|
225
|
+
async getMany<T>(keys: string[]): Promise<Map<string, CacheEntry<T> | null>> {
|
|
226
|
+
const result = new Map<string, CacheEntry<T> | null>();
|
|
227
|
+
|
|
228
|
+
if (keys.length === 0) {
|
|
229
|
+
return result;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const prefixedKeys = keys.map(k => this.prefixKey(k));
|
|
233
|
+
const values = await this.execute<(string | null)[]>(['MGET', ...prefixedKeys]);
|
|
234
|
+
|
|
235
|
+
keys.forEach((key, index) => {
|
|
236
|
+
const value = values[index];
|
|
237
|
+
if (value) {
|
|
238
|
+
try {
|
|
239
|
+
const entry = this.serializer.deserialize<CacheEntry<T>>(value);
|
|
240
|
+
if (Date.now() - entry.timestamp <= entry.ttl * 1000) {
|
|
241
|
+
this.stats.hits++;
|
|
242
|
+
result.set(key, entry);
|
|
243
|
+
} else {
|
|
244
|
+
this.stats.misses++;
|
|
245
|
+
result.set(key, null);
|
|
246
|
+
}
|
|
247
|
+
} catch {
|
|
248
|
+
this.stats.misses++;
|
|
249
|
+
result.set(key, null);
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
this.stats.misses++;
|
|
253
|
+
result.set(key, null);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Set multiple values at once
|
|
262
|
+
*/
|
|
263
|
+
async setMany<T>(entries: Map<string, CacheEntry<T>>): Promise<void> {
|
|
264
|
+
if (entries.size === 0) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Use pipeline for efficiency
|
|
269
|
+
const commands: string[][] = [];
|
|
270
|
+
|
|
271
|
+
entries.forEach((entry, key) => {
|
|
272
|
+
const prefixedKey = this.prefixKey(key);
|
|
273
|
+
const serialized = this.serializer.serialize(entry);
|
|
274
|
+
|
|
275
|
+
const expirationSeconds = this.config.staleWhileRevalidate
|
|
276
|
+
? Math.ceil(entry.ttl * (this.config.staleTolerance || 2))
|
|
277
|
+
: entry.ttl;
|
|
278
|
+
|
|
279
|
+
if (expirationSeconds > 0) {
|
|
280
|
+
commands.push(['SET', prefixedKey, serialized, 'EX', String(expirationSeconds)]);
|
|
281
|
+
} else {
|
|
282
|
+
commands.push(['SET', prefixedKey, serialized]);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
await this.pipeline(commands);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Get cache statistics
|
|
291
|
+
*/
|
|
292
|
+
async getStats(): Promise<CacheStats> {
|
|
293
|
+
let size = 0;
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const dbSize = await this.execute<number>(['DBSIZE']);
|
|
297
|
+
size = dbSize || 0;
|
|
298
|
+
} catch {
|
|
299
|
+
// Ignore stats errors
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const total = this.stats.hits + this.stats.misses;
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
hits: this.stats.hits,
|
|
306
|
+
misses: this.stats.misses,
|
|
307
|
+
size,
|
|
308
|
+
hitRatio: total > 0 ? this.stats.hits / total : 0
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Close method (no-op for REST-based client)
|
|
314
|
+
*/
|
|
315
|
+
async close(): Promise<void> {
|
|
316
|
+
// No connection to close for REST API
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Increment a counter (useful for rate limiting)
|
|
321
|
+
*/
|
|
322
|
+
async incr(key: string, ttl?: number): Promise<number> {
|
|
323
|
+
const prefixedKey = this.prefixKey(key);
|
|
324
|
+
|
|
325
|
+
if (ttl) {
|
|
326
|
+
// Use pipeline to set expiration atomically
|
|
327
|
+
const results = await this.pipeline<number>([
|
|
328
|
+
['INCR', prefixedKey],
|
|
329
|
+
['EXPIRE', prefixedKey, String(ttl)]
|
|
330
|
+
]);
|
|
331
|
+
return results[0];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return this.execute<number>(['INCR', prefixedKey]);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Set with NX (only if not exists) - useful for distributed locks
|
|
339
|
+
*/
|
|
340
|
+
async setNX(key: string, value: string, ttl?: number): Promise<boolean> {
|
|
341
|
+
const prefixedKey = this.prefixKey(key);
|
|
342
|
+
|
|
343
|
+
if (ttl) {
|
|
344
|
+
const result = await this.execute<string | null>(['SET', prefixedKey, value, 'NX', 'EX', String(ttl)]);
|
|
345
|
+
return result === 'OK';
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const result = await this.execute<number>(['SETNX', prefixedKey, value]);
|
|
349
|
+
return result === 1;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Create an Upstash cache instance
|
|
355
|
+
*/
|
|
356
|
+
export function createUpstashCache(config: UpstashCacheConfig): UpstashCache {
|
|
357
|
+
return new UpstashCache(config);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Create Upstash cache from environment variables
|
|
362
|
+
*/
|
|
363
|
+
export function createUpstashCacheFromEnv(options?: Omit<UpstashCacheConfig, 'url' | 'token'>): UpstashCache {
|
|
364
|
+
const url = process.env.UPSTASH_REDIS_REST_URL;
|
|
365
|
+
const token = process.env.UPSTASH_REDIS_REST_TOKEN;
|
|
366
|
+
|
|
367
|
+
if (!url || !token) {
|
|
368
|
+
throw new Error(
|
|
369
|
+
'Missing Upstash environment variables. ' +
|
|
370
|
+
'Please set UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN.'
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return new UpstashCache({
|
|
375
|
+
url,
|
|
376
|
+
token,
|
|
377
|
+
...options
|
|
378
|
+
});
|
|
379
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview GitHub API caching layer with stale-while-revalidate
|
|
3
|
+
* @copyright Copyright (c) 2024-2026 nirholas
|
|
4
|
+
* @license MIT
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { CacheEntry, CacheAdapter } from './types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* In-memory cache adapter for serverless environments
|
|
11
|
+
*/
|
|
12
|
+
export class MemoryCacheAdapter implements CacheAdapter {
|
|
13
|
+
private cache: Map<string, CacheEntry<any>> = new Map();
|
|
14
|
+
|
|
15
|
+
async get<T>(key: string): Promise<CacheEntry<T> | null> {
|
|
16
|
+
const entry = this.cache.get(key);
|
|
17
|
+
if (!entry) return null;
|
|
18
|
+
return entry as CacheEntry<T>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async set<T>(key: string, entry: CacheEntry<T>): Promise<void> {
|
|
22
|
+
this.cache.set(key, entry);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async delete(key: string): Promise<void> {
|
|
26
|
+
this.cache.delete(key);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async clear(): Promise<void> {
|
|
30
|
+
this.cache.clear();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get cache statistics
|
|
35
|
+
*/
|
|
36
|
+
getStats(): { size: number; keys: string[] } {
|
|
37
|
+
return {
|
|
38
|
+
size: this.cache.size,
|
|
39
|
+
keys: Array.from(this.cache.keys())
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Default TTL values in seconds
|
|
46
|
+
*/
|
|
47
|
+
export const DEFAULT_TTL = {
|
|
48
|
+
METADATA: 3600, // 1 hour for repo metadata
|
|
49
|
+
FILES: 900, // 15 minutes for file contents
|
|
50
|
+
SPECS: 1800, // 30 minutes for API specs
|
|
51
|
+
DIRECTORY: 600 // 10 minutes for directory listings
|
|
52
|
+
} as const;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* GitHub API Cache with stale-while-revalidate support
|
|
56
|
+
*/
|
|
57
|
+
export class GitHubCache {
|
|
58
|
+
private adapter: CacheAdapter;
|
|
59
|
+
private ttls: { metadata: number; files: number };
|
|
60
|
+
private verbose: boolean;
|
|
61
|
+
private revalidating: Set<string> = new Set();
|
|
62
|
+
|
|
63
|
+
constructor(
|
|
64
|
+
adapter?: CacheAdapter,
|
|
65
|
+
ttls?: { metadata?: number; files?: number },
|
|
66
|
+
verbose?: boolean
|
|
67
|
+
) {
|
|
68
|
+
this.adapter = adapter || new MemoryCacheAdapter();
|
|
69
|
+
this.ttls = {
|
|
70
|
+
metadata: ttls?.metadata || DEFAULT_TTL.METADATA,
|
|
71
|
+
files: ttls?.files || DEFAULT_TTL.FILES
|
|
72
|
+
};
|
|
73
|
+
this.verbose = verbose || false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Generate cache key from components
|
|
78
|
+
*/
|
|
79
|
+
generateKey(owner: string, repo: string, type: string, path?: string, ref?: string): string {
|
|
80
|
+
const parts = [owner, repo, type];
|
|
81
|
+
if (ref) parts.push(ref);
|
|
82
|
+
if (path) parts.push(path);
|
|
83
|
+
return parts.join('/');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Check if an entry is fresh (not stale)
|
|
88
|
+
*/
|
|
89
|
+
private isFresh(entry: CacheEntry<any>): boolean {
|
|
90
|
+
const age = Date.now() - entry.timestamp;
|
|
91
|
+
return age < entry.ttl * 1000;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Check if an entry is stale but usable (within 2x TTL)
|
|
96
|
+
*/
|
|
97
|
+
private isStale(entry: CacheEntry<any>): boolean {
|
|
98
|
+
const age = Date.now() - entry.timestamp;
|
|
99
|
+
return age >= entry.ttl * 1000 && age < entry.ttl * 2000;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get cached data with stale-while-revalidate pattern
|
|
104
|
+
* Returns cached data immediately if available (even if stale),
|
|
105
|
+
* and triggers background revalidation if stale
|
|
106
|
+
*/
|
|
107
|
+
async get<T>(
|
|
108
|
+
key: string,
|
|
109
|
+
fetcher: () => Promise<T>,
|
|
110
|
+
ttl?: number
|
|
111
|
+
): Promise<T> {
|
|
112
|
+
const entry = await this.adapter.get<T>(key);
|
|
113
|
+
|
|
114
|
+
if (entry) {
|
|
115
|
+
if (this.isFresh(entry)) {
|
|
116
|
+
if (this.verbose) {
|
|
117
|
+
console.log(`[Cache] HIT (fresh): ${key}`);
|
|
118
|
+
}
|
|
119
|
+
return entry.data;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (this.isStale(entry)) {
|
|
123
|
+
if (this.verbose) {
|
|
124
|
+
console.log(`[Cache] HIT (stale, revalidating): ${key}`);
|
|
125
|
+
}
|
|
126
|
+
// Trigger background revalidation
|
|
127
|
+
this.revalidateInBackground(key, fetcher, ttl || entry.ttl);
|
|
128
|
+
return entry.data;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Cache miss or expired beyond stale threshold
|
|
133
|
+
if (this.verbose) {
|
|
134
|
+
console.log(`[Cache] MISS: ${key}`);
|
|
135
|
+
}
|
|
136
|
+
return this.fetchAndCache(key, fetcher, ttl || this.ttls.files);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Fetch data and store in cache
|
|
141
|
+
*/
|
|
142
|
+
private async fetchAndCache<T>(
|
|
143
|
+
key: string,
|
|
144
|
+
fetcher: () => Promise<T>,
|
|
145
|
+
ttl: number
|
|
146
|
+
): Promise<T> {
|
|
147
|
+
const data = await fetcher();
|
|
148
|
+
await this.adapter.set(key, {
|
|
149
|
+
data,
|
|
150
|
+
timestamp: Date.now(),
|
|
151
|
+
ttl
|
|
152
|
+
});
|
|
153
|
+
return data;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Revalidate cache entry in background
|
|
158
|
+
*/
|
|
159
|
+
private async revalidateInBackground<T>(
|
|
160
|
+
key: string,
|
|
161
|
+
fetcher: () => Promise<T>,
|
|
162
|
+
ttl: number
|
|
163
|
+
): Promise<void> {
|
|
164
|
+
// Prevent multiple simultaneous revalidations
|
|
165
|
+
if (this.revalidating.has(key)) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.revalidating.add(key);
|
|
170
|
+
try {
|
|
171
|
+
await this.fetchAndCache(key, fetcher, ttl);
|
|
172
|
+
if (this.verbose) {
|
|
173
|
+
console.log(`[Cache] Revalidated: ${key}`);
|
|
174
|
+
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (this.verbose) {
|
|
177
|
+
console.log(`[Cache] Revalidation failed: ${key}`, error);
|
|
178
|
+
}
|
|
179
|
+
// Keep stale data on revalidation failure
|
|
180
|
+
} finally {
|
|
181
|
+
this.revalidating.delete(key);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Force refresh a cache entry
|
|
187
|
+
*/
|
|
188
|
+
async refresh<T>(
|
|
189
|
+
key: string,
|
|
190
|
+
fetcher: () => Promise<T>,
|
|
191
|
+
ttl?: number
|
|
192
|
+
): Promise<T> {
|
|
193
|
+
if (this.verbose) {
|
|
194
|
+
console.log(`[Cache] FORCE REFRESH: ${key}`);
|
|
195
|
+
}
|
|
196
|
+
return this.fetchAndCache(key, fetcher, ttl || this.ttls.files);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Invalidate a specific cache entry
|
|
201
|
+
*/
|
|
202
|
+
async invalidate(key: string): Promise<void> {
|
|
203
|
+
await this.adapter.delete(key);
|
|
204
|
+
if (this.verbose) {
|
|
205
|
+
console.log(`[Cache] INVALIDATED: ${key}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Invalidate all entries for a repository
|
|
211
|
+
*/
|
|
212
|
+
async invalidateRepo(owner: string, repo: string): Promise<void> {
|
|
213
|
+
const prefix = `${owner}/${repo}/`;
|
|
214
|
+
// For memory adapter, we can iterate
|
|
215
|
+
if (this.adapter instanceof MemoryCacheAdapter) {
|
|
216
|
+
const stats = this.adapter.getStats();
|
|
217
|
+
for (const key of stats.keys) {
|
|
218
|
+
if (key.startsWith(prefix)) {
|
|
219
|
+
await this.adapter.delete(key);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (this.verbose) {
|
|
224
|
+
console.log(`[Cache] INVALIDATED REPO: ${owner}/${repo}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Clear entire cache
|
|
230
|
+
*/
|
|
231
|
+
async clear(): Promise<void> {
|
|
232
|
+
await this.adapter.clear();
|
|
233
|
+
if (this.verbose) {
|
|
234
|
+
console.log(`[Cache] CLEARED`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get TTL for metadata
|
|
240
|
+
*/
|
|
241
|
+
get metadataTTL(): number {
|
|
242
|
+
return this.ttls.metadata;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Get TTL for files
|
|
247
|
+
*/
|
|
248
|
+
get filesTTL(): number {
|
|
249
|
+
return this.ttls.files;
|
|
250
|
+
}
|
|
251
|
+
}
|