imposters 0.1.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/LICENSE +21 -0
- package/Program/package.json +6 -0
- package/README.md +365 -0
- package/api/AdminApi/package.json +6 -0
- package/api/ApiErrors/package.json +6 -0
- package/api/ApiSchemas/package.json +6 -0
- package/api/Conversions/package.json +6 -0
- package/api/ImpostersGroup/package.json +6 -0
- package/api/ImpostersHandlers/package.json +6 -0
- package/api/SystemGroup/package.json +6 -0
- package/api/SystemHandlers/package.json +6 -0
- package/bin/imposters +47 -0
- package/cli/Commands/package.json +6 -0
- package/cli/ConfigLoader/package.json +6 -0
- package/client/HandlerHttpClient/package.json +6 -0
- package/client/ImpostersClient/package.json +6 -0
- package/client/testing/package.json +6 -0
- package/dist/cjs/Program.js +4 -0
- package/dist/cjs/Program.js.map +1 -0
- package/dist/cjs/api/AdminApi.js +11 -0
- package/dist/cjs/api/AdminApi.js.map +1 -0
- package/dist/cjs/api/ApiErrors.js +30 -0
- package/dist/cjs/api/ApiErrors.js.map +1 -0
- package/dist/cjs/api/ApiSchemas.js +36 -0
- package/dist/cjs/api/ApiSchemas.js.map +1 -0
- package/dist/cjs/api/Conversions.js +41 -0
- package/dist/cjs/api/Conversions.js.map +1 -0
- package/dist/cjs/api/ImpostersGroup.js +37 -0
- package/dist/cjs/api/ImpostersGroup.js.map +1 -0
- package/dist/cjs/api/ImpostersHandlers.js +361 -0
- package/dist/cjs/api/ImpostersHandlers.js.map +1 -0
- package/dist/cjs/api/SystemGroup.js +12 -0
- package/dist/cjs/api/SystemGroup.js.map +1 -0
- package/dist/cjs/api/SystemHandlers.js +74 -0
- package/dist/cjs/api/SystemHandlers.js.map +1 -0
- package/dist/cjs/cli/Commands.js +104 -0
- package/dist/cjs/cli/Commands.js.map +1 -0
- package/dist/cjs/cli/ConfigLoader.js +34 -0
- package/dist/cjs/cli/ConfigLoader.js.map +1 -0
- package/dist/cjs/client/HandlerHttpClient.js +50 -0
- package/dist/cjs/client/HandlerHttpClient.js.map +1 -0
- package/dist/cjs/client/ImpostersClient.js +20 -0
- package/dist/cjs/client/ImpostersClient.js.map +1 -0
- package/dist/cjs/client/index.js +57 -0
- package/dist/cjs/client/index.js.map +1 -0
- package/dist/cjs/client/testing.js +94 -0
- package/dist/cjs/client/testing.js.map +1 -0
- package/dist/cjs/domain/imposter.js +125 -0
- package/dist/cjs/domain/imposter.js.map +1 -0
- package/dist/cjs/domain/route.js +185 -0
- package/dist/cjs/domain/route.js.map +1 -0
- package/dist/cjs/index.js +106 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/layers/ApiLayer.js +18 -0
- package/dist/cjs/layers/ApiLayer.js.map +1 -0
- package/dist/cjs/layers/MainLayer.js +27 -0
- package/dist/cjs/layers/MainLayer.js.map +1 -0
- package/dist/cjs/matching/ExpressionEvaluator.js +103 -0
- package/dist/cjs/matching/ExpressionEvaluator.js.map +1 -0
- package/dist/cjs/matching/RequestMatcher.js +145 -0
- package/dist/cjs/matching/RequestMatcher.js.map +1 -0
- package/dist/cjs/matching/ResponseGenerator.js +80 -0
- package/dist/cjs/matching/ResponseGenerator.js.map +1 -0
- package/dist/cjs/matching/TemplateEngine.js +55 -0
- package/dist/cjs/matching/TemplateEngine.js.map +1 -0
- package/dist/cjs/repositories/ImposterRepository.js +118 -0
- package/dist/cjs/repositories/ImposterRepository.js.map +1 -0
- package/dist/cjs/schemas/ConfigFileSchema.js +44 -0
- package/dist/cjs/schemas/ConfigFileSchema.js.map +1 -0
- package/dist/cjs/schemas/ImposterSchema.js +202 -0
- package/dist/cjs/schemas/ImposterSchema.js.map +1 -0
- package/dist/cjs/schemas/RequestLogSchema.js +51 -0
- package/dist/cjs/schemas/RequestLogSchema.js.map +1 -0
- package/dist/cjs/schemas/StubSchema.js +84 -0
- package/dist/cjs/schemas/StubSchema.js.map +1 -0
- package/dist/cjs/schemas/common.js +67 -0
- package/dist/cjs/schemas/common.js.map +1 -0
- package/dist/cjs/server/AdminServer.js +36 -0
- package/dist/cjs/server/AdminServer.js.map +1 -0
- package/dist/cjs/server/BunServer.js +13 -0
- package/dist/cjs/server/BunServer.js.map +1 -0
- package/dist/cjs/server/FiberManager.js +21 -0
- package/dist/cjs/server/FiberManager.js.map +1 -0
- package/dist/cjs/server/ImposterServer.js +234 -0
- package/dist/cjs/server/ImposterServer.js.map +1 -0
- package/dist/cjs/services/AppConfig.js +18 -0
- package/dist/cjs/services/AppConfig.js.map +1 -0
- package/dist/cjs/services/MetricsService.js +113 -0
- package/dist/cjs/services/MetricsService.js.map +1 -0
- package/dist/cjs/services/PortAllocator.js +50 -0
- package/dist/cjs/services/PortAllocator.js.map +1 -0
- package/dist/cjs/services/ProxyService.js +109 -0
- package/dist/cjs/services/ProxyService.js.map +1 -0
- package/dist/cjs/services/RequestLogger.js +60 -0
- package/dist/cjs/services/RequestLogger.js.map +1 -0
- package/dist/cjs/services/Uuid.js +10 -0
- package/dist/cjs/services/Uuid.js.map +1 -0
- package/dist/cjs/services/UuidLive.js +16 -0
- package/dist/cjs/services/UuidLive.js.map +1 -0
- package/dist/cjs/ui/UiRouter.js +242 -0
- package/dist/cjs/ui/UiRouter.js.map +1 -0
- package/dist/cjs/ui/admin/AdminLayout.js +36 -0
- package/dist/cjs/ui/admin/AdminLayout.js.map +1 -0
- package/dist/cjs/ui/admin/AdminUiRouter.js +155 -0
- package/dist/cjs/ui/admin/AdminUiRouter.js.map +1 -0
- package/dist/cjs/ui/admin/pages/AdminDashboard.js +55 -0
- package/dist/cjs/ui/admin/pages/AdminDashboard.js.map +1 -0
- package/dist/cjs/ui/admin/partials.js +64 -0
- package/dist/cjs/ui/admin/partials.js.map +1 -0
- package/dist/cjs/ui/html.js +42 -0
- package/dist/cjs/ui/html.js.map +1 -0
- package/dist/cjs/ui/layout.js +39 -0
- package/dist/cjs/ui/layout.js.map +1 -0
- package/dist/cjs/ui/pages/dashboard.js +51 -0
- package/dist/cjs/ui/pages/dashboard.js.map +1 -0
- package/dist/cjs/ui/pages/request-detail.js +119 -0
- package/dist/cjs/ui/pages/request-detail.js.map +1 -0
- package/dist/cjs/ui/pages/requests.js +120 -0
- package/dist/cjs/ui/pages/requests.js.map +1 -0
- package/dist/cjs/ui/pages/stubs.js +46 -0
- package/dist/cjs/ui/pages/stubs.js.map +1 -0
- package/dist/cjs/ui/partials.js +104 -0
- package/dist/cjs/ui/partials.js.map +1 -0
- package/dist/dts/Program.d.ts +2 -0
- package/dist/dts/Program.d.ts.map +1 -0
- package/dist/dts/api/AdminApi.d.ts +490 -0
- package/dist/dts/api/AdminApi.d.ts.map +1 -0
- package/dist/dts/api/ApiErrors.d.ts +26 -0
- package/dist/dts/api/ApiErrors.d.ts.map +1 -0
- package/dist/dts/api/ApiSchemas.d.ts +36 -0
- package/dist/dts/api/ApiSchemas.d.ts.map +1 -0
- package/dist/dts/api/Conversions.d.ts +7 -0
- package/dist/dts/api/Conversions.d.ts.map +1 -0
- package/dist/dts/api/ImpostersGroup.d.ts +448 -0
- package/dist/dts/api/ImpostersGroup.d.ts.map +1 -0
- package/dist/dts/api/ImpostersHandlers.d.ts +9 -0
- package/dist/dts/api/ImpostersHandlers.d.ts.map +1 -0
- package/dist/dts/api/SystemGroup.d.ts +46 -0
- package/dist/dts/api/SystemGroup.d.ts.map +1 -0
- package/dist/dts/api/SystemHandlers.d.ts +4 -0
- package/dist/dts/api/SystemHandlers.d.ts.map +1 -0
- package/dist/dts/cli/Commands.d.ts +4 -0
- package/dist/dts/cli/Commands.d.ts.map +1 -0
- package/dist/dts/cli/ConfigLoader.d.ts +13 -0
- package/dist/dts/cli/ConfigLoader.d.ts.map +1 -0
- package/dist/dts/client/HandlerHttpClient.d.ts +5 -0
- package/dist/dts/client/HandlerHttpClient.d.ts.map +1 -0
- package/dist/dts/client/ImpostersClient.d.ts +1868 -0
- package/dist/dts/client/ImpostersClient.d.ts.map +1 -0
- package/dist/dts/client/index.d.ts +6 -0
- package/dist/dts/client/index.d.ts.map +1 -0
- package/dist/dts/client/testing.d.ts +35 -0
- package/dist/dts/client/testing.d.ts.map +1 -0
- package/dist/dts/domain/imposter.d.ts +123 -0
- package/dist/dts/domain/imposter.d.ts.map +1 -0
- package/dist/dts/domain/route.d.ts +128 -0
- package/dist/dts/domain/route.d.ts.map +1 -0
- package/dist/dts/index.d.ts +60 -0
- package/dist/dts/index.d.ts.map +1 -0
- package/dist/dts/layers/ApiLayer.d.ts +3 -0
- package/dist/dts/layers/ApiLayer.d.ts.map +1 -0
- package/dist/dts/layers/MainLayer.d.ts +3 -0
- package/dist/dts/layers/MainLayer.d.ts.map +1 -0
- package/dist/dts/matching/ExpressionEvaluator.d.ts +11 -0
- package/dist/dts/matching/ExpressionEvaluator.d.ts.map +1 -0
- package/dist/dts/matching/RequestMatcher.d.ts +13 -0
- package/dist/dts/matching/RequestMatcher.d.ts.map +1 -0
- package/dist/dts/matching/ResponseGenerator.d.ts +9 -0
- package/dist/dts/matching/ResponseGenerator.d.ts.map +1 -0
- package/dist/dts/matching/TemplateEngine.d.ts +4 -0
- package/dist/dts/matching/TemplateEngine.d.ts.map +1 -0
- package/dist/dts/repositories/ImposterRepository.d.ts +33 -0
- package/dist/dts/repositories/ImposterRepository.d.ts.map +1 -0
- package/dist/dts/schemas/ConfigFileSchema.d.ts +142 -0
- package/dist/dts/schemas/ConfigFileSchema.d.ts.map +1 -0
- package/dist/dts/schemas/ImposterSchema.d.ts +368 -0
- package/dist/dts/schemas/ImposterSchema.d.ts.map +1 -0
- package/dist/dts/schemas/RequestLogSchema.d.ts +36 -0
- package/dist/dts/schemas/RequestLogSchema.d.ts.map +1 -0
- package/dist/dts/schemas/StubSchema.d.ts +112 -0
- package/dist/dts/schemas/StubSchema.d.ts.map +1 -0
- package/dist/dts/schemas/common.d.ts +56 -0
- package/dist/dts/schemas/common.d.ts.map +1 -0
- package/dist/dts/server/AdminServer.d.ts +11 -0
- package/dist/dts/server/AdminServer.d.ts.map +1 -0
- package/dist/dts/server/BunServer.d.ts +17 -0
- package/dist/dts/server/BunServer.d.ts.map +1 -0
- package/dist/dts/server/FiberManager.d.ts +12 -0
- package/dist/dts/server/FiberManager.d.ts.map +1 -0
- package/dist/dts/server/ImposterServer.d.ts +29 -0
- package/dist/dts/server/ImposterServer.d.ts.map +1 -0
- package/dist/dts/services/AppConfig.d.ts +14 -0
- package/dist/dts/services/AppConfig.d.ts.map +1 -0
- package/dist/dts/services/MetricsService.d.ts +26 -0
- package/dist/dts/services/MetricsService.d.ts.map +1 -0
- package/dist/dts/services/PortAllocator.d.ts +29 -0
- package/dist/dts/services/PortAllocator.d.ts.map +1 -0
- package/dist/dts/services/ProxyService.d.ts +24 -0
- package/dist/dts/services/ProxyService.d.ts.map +1 -0
- package/dist/dts/services/RequestLogger.d.ts +23 -0
- package/dist/dts/services/RequestLogger.d.ts.map +1 -0
- package/dist/dts/services/Uuid.d.ts +9 -0
- package/dist/dts/services/Uuid.d.ts.map +1 -0
- package/dist/dts/services/UuidLive.d.ts +4 -0
- package/dist/dts/services/UuidLive.d.ts.map +1 -0
- package/dist/dts/ui/UiRouter.d.ts +15 -0
- package/dist/dts/ui/UiRouter.d.ts.map +1 -0
- package/dist/dts/ui/admin/AdminLayout.d.ts +7 -0
- package/dist/dts/ui/admin/AdminLayout.d.ts.map +1 -0
- package/dist/dts/ui/admin/AdminUiRouter.d.ts +6 -0
- package/dist/dts/ui/admin/AdminUiRouter.d.ts.map +1 -0
- package/dist/dts/ui/admin/pages/AdminDashboard.d.ts +7 -0
- package/dist/dts/ui/admin/pages/AdminDashboard.d.ts.map +1 -0
- package/dist/dts/ui/admin/partials.d.ts +14 -0
- package/dist/dts/ui/admin/partials.d.ts.map +1 -0
- package/dist/dts/ui/html.d.ts +12 -0
- package/dist/dts/ui/html.d.ts.map +1 -0
- package/dist/dts/ui/layout.d.ts +9 -0
- package/dist/dts/ui/layout.d.ts.map +1 -0
- package/dist/dts/ui/pages/dashboard.d.ts +10 -0
- package/dist/dts/ui/pages/dashboard.d.ts.map +1 -0
- package/dist/dts/ui/pages/request-detail.d.ts +11 -0
- package/dist/dts/ui/pages/request-detail.d.ts.map +1 -0
- package/dist/dts/ui/pages/requests.d.ts +15 -0
- package/dist/dts/ui/pages/requests.d.ts.map +1 -0
- package/dist/dts/ui/pages/stubs.d.ts +8 -0
- package/dist/dts/ui/pages/stubs.d.ts.map +1 -0
- package/dist/dts/ui/partials.d.ts +13 -0
- package/dist/dts/ui/partials.d.ts.map +1 -0
- package/dist/esm/Program.js +2 -0
- package/dist/esm/Program.js.map +1 -0
- package/dist/esm/api/AdminApi.js +5 -0
- package/dist/esm/api/AdminApi.js.map +1 -0
- package/dist/esm/api/ApiErrors.js +20 -0
- package/dist/esm/api/ApiErrors.js.map +1 -0
- package/dist/esm/api/ApiSchemas.js +29 -0
- package/dist/esm/api/ApiSchemas.js.map +1 -0
- package/dist/esm/api/Conversions.js +32 -0
- package/dist/esm/api/Conversions.js.map +1 -0
- package/dist/esm/api/ImpostersGroup.js +30 -0
- package/dist/esm/api/ImpostersGroup.js.map +1 -0
- package/dist/esm/api/ImpostersHandlers.js +354 -0
- package/dist/esm/api/ImpostersHandlers.js.map +1 -0
- package/dist/esm/api/SystemGroup.js +6 -0
- package/dist/esm/api/SystemGroup.js.map +1 -0
- package/dist/esm/api/SystemHandlers.js +67 -0
- package/dist/esm/api/SystemHandlers.js.map +1 -0
- package/dist/esm/cli/Commands.js +98 -0
- package/dist/esm/cli/Commands.js.map +1 -0
- package/dist/esm/cli/ConfigLoader.js +25 -0
- package/dist/esm/cli/ConfigLoader.js.map +1 -0
- package/dist/esm/client/HandlerHttpClient.js +42 -0
- package/dist/esm/client/HandlerHttpClient.js.map +1 -0
- package/dist/esm/client/ImpostersClient.js +10 -0
- package/dist/esm/client/ImpostersClient.js.map +1 -0
- package/dist/esm/client/index.js +4 -0
- package/dist/esm/client/index.js.map +1 -0
- package/dist/esm/client/testing.js +86 -0
- package/dist/esm/client/testing.js.map +1 -0
- package/dist/esm/domain/imposter.js +103 -0
- package/dist/esm/domain/imposter.js.map +1 -0
- package/dist/esm/domain/route.js +164 -0
- package/dist/esm/domain/route.js.map +1 -0
- package/dist/esm/index.js +60 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/layers/ApiLayer.js +11 -0
- package/dist/esm/layers/ApiLayer.js.map +1 -0
- package/dist/esm/layers/MainLayer.js +20 -0
- package/dist/esm/layers/MainLayer.js.map +1 -0
- package/dist/esm/matching/ExpressionEvaluator.js +94 -0
- package/dist/esm/matching/ExpressionEvaluator.js.map +1 -0
- package/dist/esm/matching/RequestMatcher.js +135 -0
- package/dist/esm/matching/RequestMatcher.js.map +1 -0
- package/dist/esm/matching/ResponseGenerator.js +71 -0
- package/dist/esm/matching/ResponseGenerator.js.map +1 -0
- package/dist/esm/matching/TemplateEngine.js +47 -0
- package/dist/esm/matching/TemplateEngine.js.map +1 -0
- package/dist/esm/package.json +4 -0
- package/dist/esm/repositories/ImposterRepository.js +110 -0
- package/dist/esm/repositories/ImposterRepository.js.map +1 -0
- package/dist/esm/schemas/ConfigFileSchema.js +37 -0
- package/dist/esm/schemas/ConfigFileSchema.js.map +1 -0
- package/dist/esm/schemas/ImposterSchema.js +195 -0
- package/dist/esm/schemas/ImposterSchema.js.map +1 -0
- package/dist/esm/schemas/RequestLogSchema.js +44 -0
- package/dist/esm/schemas/RequestLogSchema.js.map +1 -0
- package/dist/esm/schemas/StubSchema.js +77 -0
- package/dist/esm/schemas/StubSchema.js.map +1 -0
- package/dist/esm/schemas/common.js +59 -0
- package/dist/esm/schemas/common.js.map +1 -0
- package/dist/esm/server/AdminServer.js +27 -0
- package/dist/esm/server/AdminServer.js.map +1 -0
- package/dist/esm/server/BunServer.js +6 -0
- package/dist/esm/server/BunServer.js.map +1 -0
- package/dist/esm/server/FiberManager.js +14 -0
- package/dist/esm/server/FiberManager.js.map +1 -0
- package/dist/esm/server/ImposterServer.js +225 -0
- package/dist/esm/server/ImposterServer.js.map +1 -0
- package/dist/esm/services/AppConfig.js +11 -0
- package/dist/esm/services/AppConfig.js.map +1 -0
- package/dist/esm/services/MetricsService.js +105 -0
- package/dist/esm/services/MetricsService.js.map +1 -0
- package/dist/esm/services/PortAllocator.js +41 -0
- package/dist/esm/services/PortAllocator.js.map +1 -0
- package/dist/esm/services/ProxyService.js +101 -0
- package/dist/esm/services/ProxyService.js.map +1 -0
- package/dist/esm/services/RequestLogger.js +53 -0
- package/dist/esm/services/RequestLogger.js.map +1 -0
- package/dist/esm/services/Uuid.js +3 -0
- package/dist/esm/services/Uuid.js.map +1 -0
- package/dist/esm/services/UuidLive.js +9 -0
- package/dist/esm/services/UuidLive.js.map +1 -0
- package/dist/esm/ui/UiRouter.js +235 -0
- package/dist/esm/ui/UiRouter.js.map +1 -0
- package/dist/esm/ui/admin/AdminLayout.js +29 -0
- package/dist/esm/ui/admin/AdminLayout.js.map +1 -0
- package/dist/esm/ui/admin/AdminUiRouter.js +148 -0
- package/dist/esm/ui/admin/AdminUiRouter.js.map +1 -0
- package/dist/esm/ui/admin/pages/AdminDashboard.js +48 -0
- package/dist/esm/ui/admin/pages/AdminDashboard.js.map +1 -0
- package/dist/esm/ui/admin/partials.js +54 -0
- package/dist/esm/ui/admin/partials.js.map +1 -0
- package/dist/esm/ui/html.js +32 -0
- package/dist/esm/ui/html.js.map +1 -0
- package/dist/esm/ui/layout.js +32 -0
- package/dist/esm/ui/layout.js.map +1 -0
- package/dist/esm/ui/pages/dashboard.js +44 -0
- package/dist/esm/ui/pages/dashboard.js.map +1 -0
- package/dist/esm/ui/pages/request-detail.js +112 -0
- package/dist/esm/ui/pages/request-detail.js.map +1 -0
- package/dist/esm/ui/pages/requests.js +112 -0
- package/dist/esm/ui/pages/requests.js.map +1 -0
- package/dist/esm/ui/pages/stubs.js +39 -0
- package/dist/esm/ui/pages/stubs.js.map +1 -0
- package/dist/esm/ui/partials.js +91 -0
- package/dist/esm/ui/partials.js.map +1 -0
- package/domain/imposter/package.json +6 -0
- package/domain/route/package.json +6 -0
- package/layers/ApiLayer/package.json +6 -0
- package/layers/MainLayer/package.json +6 -0
- package/matching/ExpressionEvaluator/package.json +6 -0
- package/matching/RequestMatcher/package.json +6 -0
- package/matching/ResponseGenerator/package.json +6 -0
- package/matching/TemplateEngine/package.json +6 -0
- package/package.json +435 -0
- package/repositories/ImposterRepository/package.json +6 -0
- package/schemas/ConfigFileSchema/package.json +6 -0
- package/schemas/ImposterSchema/package.json +6 -0
- package/schemas/RequestLogSchema/package.json +6 -0
- package/schemas/StubSchema/package.json +6 -0
- package/schemas/common/package.json +6 -0
- package/server/AdminServer/package.json +6 -0
- package/server/BunServer/package.json +6 -0
- package/server/FiberManager/package.json +6 -0
- package/server/ImposterServer/package.json +6 -0
- package/services/AppConfig/package.json +6 -0
- package/services/MetricsService/package.json +6 -0
- package/services/PortAllocator/package.json +6 -0
- package/services/ProxyService/package.json +6 -0
- package/services/RequestLogger/package.json +6 -0
- package/services/Uuid/package.json +6 -0
- package/services/UuidLive/package.json +6 -0
- package/src/Program.ts +1 -0
- package/src/api/AdminApi.ts +7 -0
- package/src/api/ApiErrors.ts +20 -0
- package/src/api/ApiSchemas.ts +36 -0
- package/src/api/Conversions.ts +34 -0
- package/src/api/ImpostersGroup.ts +103 -0
- package/src/api/ImpostersHandlers.ts +387 -0
- package/src/api/SystemGroup.ts +12 -0
- package/src/api/SystemHandlers.ts +76 -0
- package/src/cli/Commands.ts +119 -0
- package/src/cli/ConfigLoader.ts +41 -0
- package/src/client/HandlerHttpClient.ts +50 -0
- package/src/client/ImpostersClient.ts +21 -0
- package/src/client/index.ts +9 -0
- package/src/client/testing.ts +105 -0
- package/src/domain/imposter.ts +186 -0
- package/src/domain/route.ts +255 -0
- package/src/index.ts +153 -0
- package/src/layers/ApiLayer.ts +21 -0
- package/src/layers/MainLayer.ts +43 -0
- package/src/matching/ExpressionEvaluator.ts +102 -0
- package/src/matching/RequestMatcher.ts +162 -0
- package/src/matching/ResponseGenerator.ts +86 -0
- package/src/matching/TemplateEngine.ts +54 -0
- package/src/repositories/ImposterRepository.ts +145 -0
- package/src/schemas/ConfigFileSchema.ts +32 -0
- package/src/schemas/ImposterSchema.ts +232 -0
- package/src/schemas/RequestLogSchema.ts +38 -0
- package/src/schemas/StubSchema.ts +90 -0
- package/src/schemas/common.ts +95 -0
- package/src/server/AdminServer.ts +22 -0
- package/src/server/BunServer.ts +19 -0
- package/src/server/FiberManager.ts +25 -0
- package/src/server/ImposterServer.ts +244 -0
- package/src/services/AppConfig.ts +22 -0
- package/src/services/MetricsService.ts +157 -0
- package/src/services/PortAllocator.ts +68 -0
- package/src/services/ProxyService.ts +139 -0
- package/src/services/RequestLogger.ts +87 -0
- package/src/services/Uuid.ts +9 -0
- package/src/services/UuidLive.ts +9 -0
- package/src/types/bun.d.ts +6 -0
- package/src/ui/UiRouter.ts +278 -0
- package/src/ui/admin/AdminLayout.ts +36 -0
- package/src/ui/admin/AdminUiRouter.ts +170 -0
- package/src/ui/admin/pages/AdminDashboard.ts +54 -0
- package/src/ui/admin/partials.ts +83 -0
- package/src/ui/html.ts +37 -0
- package/src/ui/layout.ts +44 -0
- package/src/ui/pages/dashboard.ts +64 -0
- package/src/ui/pages/request-detail.ts +142 -0
- package/src/ui/pages/requests.ts +141 -0
- package/src/ui/pages/stubs.ts +52 -0
- package/src/ui/partials.ts +133 -0
- package/ui/UiRouter/package.json +6 -0
- package/ui/admin/AdminLayout/package.json +6 -0
- package/ui/admin/AdminUiRouter/package.json +6 -0
- package/ui/admin/pages/AdminDashboard/package.json +6 -0
- package/ui/admin/partials/package.json +6 -0
- package/ui/html/package.json +6 -0
- package/ui/layout/package.json +6 -0
- package/ui/pages/dashboard/package.json +6 -0
- package/ui/pages/requests/package.json +6 -0
- package/ui/pages/stubs/package.json +6 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Context, Effect, FiberMap, Layer } from "effect"
|
|
2
|
+
|
|
3
|
+
export interface FiberManagerShape {
|
|
4
|
+
readonly start: (id: string, effect: Effect.Effect<never, unknown>) => Effect.Effect<void>
|
|
5
|
+
readonly stop: (id: string) => Effect.Effect<void>
|
|
6
|
+
readonly isRunning: (id: string) => Effect.Effect<boolean>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class FiberManager extends Context.Tag("FiberManager")<FiberManager, FiberManagerShape>() {}
|
|
10
|
+
|
|
11
|
+
export const FiberManagerLive = Layer.scoped(
|
|
12
|
+
FiberManager,
|
|
13
|
+
Effect.gen(function*() {
|
|
14
|
+
const fiberMap = yield* FiberMap.make<string>()
|
|
15
|
+
|
|
16
|
+
const start = (id: string, effect: Effect.Effect<never, unknown>): Effect.Effect<void> =>
|
|
17
|
+
FiberMap.run(fiberMap, id, effect)
|
|
18
|
+
|
|
19
|
+
const stop = (id: string): Effect.Effect<void> => FiberMap.remove(fiberMap, id)
|
|
20
|
+
|
|
21
|
+
const isRunning = (id: string): Effect.Effect<boolean> => FiberMap.has(fiberMap, id)
|
|
22
|
+
|
|
23
|
+
return { start, stop, isRunning }
|
|
24
|
+
})
|
|
25
|
+
)
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { Context, Data, Effect, HashMap, Layer, Ref, Runtime } from "effect"
|
|
2
|
+
import * as DateTime from "effect/DateTime"
|
|
3
|
+
import { ImposterConfig, type ImposterNotFoundError, type ProxyConfigDomain } from "../domain/imposter"
|
|
4
|
+
import { extractRequestContext, findMatchingStub } from "../matching/RequestMatcher"
|
|
5
|
+
import { buildResponse, makeResponseState } from "../matching/ResponseGenerator"
|
|
6
|
+
import { ImposterRepository } from "../repositories/ImposterRepository"
|
|
7
|
+
import { NonEmptyString } from "../schemas/common"
|
|
8
|
+
import type { RequestLogEntry } from "../schemas/RequestLogSchema"
|
|
9
|
+
import type { Stub } from "../schemas/StubSchema"
|
|
10
|
+
import { MetricsService } from "../services/MetricsService"
|
|
11
|
+
import { ProxyService } from "../services/ProxyService"
|
|
12
|
+
import { RequestLogger } from "../services/RequestLogger"
|
|
13
|
+
import { makeUiRouter } from "../ui/UiRouter"
|
|
14
|
+
import { ServerFactory } from "./BunServer"
|
|
15
|
+
import { FiberManager } from "./FiberManager"
|
|
16
|
+
|
|
17
|
+
export class ImposterServerError extends Data.TaggedError("ImposterServerError")<{
|
|
18
|
+
readonly imposterId: string
|
|
19
|
+
readonly reason: string
|
|
20
|
+
}> {}
|
|
21
|
+
|
|
22
|
+
export interface ImposterServerShape {
|
|
23
|
+
readonly start: (id: string) => Effect.Effect<void, ImposterServerError | ImposterNotFoundError>
|
|
24
|
+
readonly stop: (id: string) => Effect.Effect<void>
|
|
25
|
+
readonly updateStubs: (id: string) => Effect.Effect<void>
|
|
26
|
+
readonly updateProxyConfig: (id: string) => Effect.Effect<void>
|
|
27
|
+
readonly isRunning: (id: string) => Effect.Effect<boolean>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class ImposterServer extends Context.Tag("ImposterServer")<ImposterServer, ImposterServerShape>() {}
|
|
31
|
+
|
|
32
|
+
interface ImposterState {
|
|
33
|
+
readonly stubsRef: Ref.Ref<ReadonlyArray<Stub>>
|
|
34
|
+
readonly proxyConfigRef: Ref.Ref<ProxyConfigDomain | undefined>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const ImposterServerLive = Layer.effect(
|
|
38
|
+
ImposterServer,
|
|
39
|
+
Effect.gen(function*() {
|
|
40
|
+
const repo = yield* ImposterRepository
|
|
41
|
+
const fiberManager = yield* FiberManager
|
|
42
|
+
const serverFactory = yield* ServerFactory
|
|
43
|
+
const requestLogger = yield* RequestLogger
|
|
44
|
+
const metricsService = yield* MetricsService
|
|
45
|
+
const proxyService = yield* ProxyService
|
|
46
|
+
const stateMapRef = yield* Ref.make<HashMap.HashMap<string, ImposterState>>(HashMap.empty())
|
|
47
|
+
|
|
48
|
+
const start = (id: string): Effect.Effect<void, ImposterServerError | ImposterNotFoundError> =>
|
|
49
|
+
Effect.gen(function*() {
|
|
50
|
+
const record = yield* repo.get(id)
|
|
51
|
+
const config = record.config
|
|
52
|
+
|
|
53
|
+
// Create per-imposter state
|
|
54
|
+
const stubsRef = yield* Ref.make<ReadonlyArray<Stub>>(record.stubs)
|
|
55
|
+
const proxyConfigRef = yield* Ref.make<ProxyConfigDomain | undefined>(config.proxy)
|
|
56
|
+
const responseState = yield* makeResponseState()
|
|
57
|
+
|
|
58
|
+
// Store state for hot-reload
|
|
59
|
+
yield* Ref.update(stateMapRef, HashMap.set(id, { stubsRef, proxyConfigRef } as ImposterState))
|
|
60
|
+
|
|
61
|
+
// Capture runtime for running effects inside fetch handler
|
|
62
|
+
const rt = yield* Effect.runtime<never>()
|
|
63
|
+
const runPromise = Runtime.runPromise(rt)
|
|
64
|
+
|
|
65
|
+
// UI router for /_admin pages
|
|
66
|
+
const uiRouter = makeUiRouter({ id, config, stubsRef, repo, requestLogger, runPromise })
|
|
67
|
+
|
|
68
|
+
const handler = async (request: Request): Promise<Response> => {
|
|
69
|
+
// Try UI router first (returns null if not a /_admin path)
|
|
70
|
+
const uiResponse = await uiRouter(request)
|
|
71
|
+
if (uiResponse !== null) return uiResponse
|
|
72
|
+
|
|
73
|
+
return runPromise(
|
|
74
|
+
Effect.gen(function*() {
|
|
75
|
+
const startTime = Date.now()
|
|
76
|
+
const stubs = yield* Ref.get(stubsRef)
|
|
77
|
+
const ctx = yield* Effect.promise(() => extractRequestContext(request))
|
|
78
|
+
const stub = findMatchingStub(ctx, stubs)
|
|
79
|
+
|
|
80
|
+
let response: Response
|
|
81
|
+
let proxied = false
|
|
82
|
+
if (!stub) {
|
|
83
|
+
const proxyConfig = yield* Ref.get(proxyConfigRef)
|
|
84
|
+
if (proxyConfig) {
|
|
85
|
+
const url = new URL(request.url)
|
|
86
|
+
response = yield* proxyService.forward(ctx, proxyConfig, url).pipe(
|
|
87
|
+
Effect.catchTag("ProxyError", (err) =>
|
|
88
|
+
Effect.succeed(
|
|
89
|
+
new Response(
|
|
90
|
+
JSON.stringify({ error: "Proxy failed", target: err.targetUrl, reason: err.reason }),
|
|
91
|
+
{ status: 502, headers: { "content-type": "application/json" } }
|
|
92
|
+
)
|
|
93
|
+
))
|
|
94
|
+
)
|
|
95
|
+
proxied = true
|
|
96
|
+
// Record mode: save as stub + update stubsRef
|
|
97
|
+
if (proxyConfig.mode === "record" && response.status < 500) {
|
|
98
|
+
const responseClone = response.clone()
|
|
99
|
+
const newStub = yield* proxyService.recordAsStub(ctx, responseClone)
|
|
100
|
+
yield* repo.addStub(id, newStub).pipe(Effect.catchAll(() => Effect.void))
|
|
101
|
+
const freshStubs = yield* repo.getStubs(id).pipe(
|
|
102
|
+
Effect.catchAll(() => Effect.succeed([] as ReadonlyArray<Stub>))
|
|
103
|
+
)
|
|
104
|
+
yield* Ref.set(stubsRef, freshStubs)
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
response = new Response(
|
|
108
|
+
JSON.stringify({ error: "No matching stub found", method: ctx.method, path: ctx.path }),
|
|
109
|
+
{ status: 404, headers: { "content-type": "application/json" } }
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
const responses = stub.responses
|
|
114
|
+
const index = yield* responseState.getNextIndex(id, stub.id, responses.length, stub.responseMode)
|
|
115
|
+
const responseConfig = responses[index]!
|
|
116
|
+
const delay = responseConfig.delay
|
|
117
|
+
if (delay !== undefined && delay > 0) {
|
|
118
|
+
yield* Effect.sleep(`${delay} millis`)
|
|
119
|
+
}
|
|
120
|
+
response = yield* Effect.promise(() => buildResponse(responseConfig, ctx))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Capture response for logging
|
|
124
|
+
const respText = yield* Effect.promise(() => response.text())
|
|
125
|
+
const respHeaders: Record<string, string> = {}
|
|
126
|
+
response.headers.forEach((val, key) => {
|
|
127
|
+
respHeaders[key] = val
|
|
128
|
+
})
|
|
129
|
+
// Reconstruct since .text() consumed body
|
|
130
|
+
response = new Response(respText, { status: response.status, headers: response.headers })
|
|
131
|
+
|
|
132
|
+
const logBody = respText.length > 10240 ? respText.slice(0, 10240) : (respText || undefined)
|
|
133
|
+
|
|
134
|
+
const duration = Date.now() - startTime
|
|
135
|
+
const logEntry: RequestLogEntry = {
|
|
136
|
+
id: NonEmptyString.make(crypto.randomUUID()),
|
|
137
|
+
imposterId: NonEmptyString.make(id),
|
|
138
|
+
timestamp: DateTime.unsafeMake(startTime),
|
|
139
|
+
request: {
|
|
140
|
+
method: ctx.method,
|
|
141
|
+
path: ctx.path,
|
|
142
|
+
headers: ctx.headers,
|
|
143
|
+
query: ctx.query,
|
|
144
|
+
body: ctx.body
|
|
145
|
+
},
|
|
146
|
+
response: {
|
|
147
|
+
status: response.status,
|
|
148
|
+
headers: respHeaders,
|
|
149
|
+
...(logBody !== undefined ? { body: logBody } : {}),
|
|
150
|
+
...(stub ? { matchedStubId: NonEmptyString.make(stub.id) } : {}),
|
|
151
|
+
proxied
|
|
152
|
+
},
|
|
153
|
+
duration
|
|
154
|
+
}
|
|
155
|
+
yield* requestLogger.log(logEntry).pipe(Effect.catchAll(() => Effect.void))
|
|
156
|
+
yield* metricsService.recordRequest(logEntry).pipe(Effect.catchAll(() => Effect.void))
|
|
157
|
+
|
|
158
|
+
return response
|
|
159
|
+
}).pipe(
|
|
160
|
+
Effect.catchAllCause((cause) =>
|
|
161
|
+
Effect.succeed(
|
|
162
|
+
new Response(
|
|
163
|
+
JSON.stringify({ error: "Internal server error", details: String(cause) }),
|
|
164
|
+
{ status: 500, headers: { "content-type": "application/json" } }
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Build the long-running fiber effect with acquireRelease
|
|
173
|
+
const fiberEffect = Effect.acquireRelease(
|
|
174
|
+
Effect.try({
|
|
175
|
+
try: () => serverFactory.create({ port: config.port, fetch: handler }),
|
|
176
|
+
catch: (err) =>
|
|
177
|
+
new ImposterServerError({ imposterId: id, reason: `Failed to bind port ${config.port}: ${err}` })
|
|
178
|
+
}),
|
|
179
|
+
(server) => Effect.sync(() => server.stop(true))
|
|
180
|
+
).pipe(
|
|
181
|
+
Effect.andThen(Effect.never),
|
|
182
|
+
Effect.scoped
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
// Wrap fiber in onError for crash supervision
|
|
186
|
+
const supervisedEffect = fiberEffect.pipe(
|
|
187
|
+
Effect.onError(() =>
|
|
188
|
+
Effect.gen(function*() {
|
|
189
|
+
yield* Ref.update(stateMapRef, HashMap.remove(id))
|
|
190
|
+
yield* repo.update(id, (r) => ({
|
|
191
|
+
...r,
|
|
192
|
+
config: ImposterConfig({ ...r.config, status: "stopped" })
|
|
193
|
+
})).pipe(Effect.catchAll(() => Effect.void))
|
|
194
|
+
yield* responseState.reset(id)
|
|
195
|
+
})
|
|
196
|
+
)
|
|
197
|
+
) as Effect.Effect<never, unknown>
|
|
198
|
+
|
|
199
|
+
yield* fiberManager.start(id, supervisedEffect)
|
|
200
|
+
|
|
201
|
+
// Update status to running
|
|
202
|
+
yield* repo.update(id, (r) => ({
|
|
203
|
+
...r,
|
|
204
|
+
config: ImposterConfig({ ...r.config, status: "running" })
|
|
205
|
+
})).pipe(Effect.catchTag("ImposterNotFoundError", () => Effect.void))
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
const stop = (id: string): Effect.Effect<void> =>
|
|
209
|
+
Effect.gen(function*() {
|
|
210
|
+
yield* fiberManager.stop(id)
|
|
211
|
+
yield* Ref.update(stateMapRef, HashMap.remove(id))
|
|
212
|
+
yield* repo.update(id, (r) => ({
|
|
213
|
+
...r,
|
|
214
|
+
config: ImposterConfig({ ...r.config, status: "stopped" })
|
|
215
|
+
})).pipe(Effect.catchAll(() => Effect.void))
|
|
216
|
+
yield* requestLogger.removeImposter(id)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
const updateStubs = (id: string): Effect.Effect<void> =>
|
|
220
|
+
Effect.gen(function*() {
|
|
221
|
+
const stubs = yield* repo.getStubs(id).pipe(Effect.catchAll(() => Effect.succeed([] as ReadonlyArray<Stub>)))
|
|
222
|
+
const stateMap = yield* Ref.get(stateMapRef)
|
|
223
|
+
const state = HashMap.get(stateMap, id)
|
|
224
|
+
if (state._tag === "Some") {
|
|
225
|
+
yield* Ref.set(state.value.stubsRef, stubs)
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
const updateProxyConfig = (id: string): Effect.Effect<void> =>
|
|
230
|
+
Effect.gen(function*() {
|
|
231
|
+
const record = yield* repo.get(id).pipe(Effect.catchAll(() => Effect.succeed(null)))
|
|
232
|
+
if (record === null) return
|
|
233
|
+
const stateMap = yield* Ref.get(stateMapRef)
|
|
234
|
+
const state = HashMap.get(stateMap, id)
|
|
235
|
+
if (state._tag === "Some") {
|
|
236
|
+
yield* Ref.set(state.value.proxyConfigRef, record.config.proxy)
|
|
237
|
+
}
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
const isRunning = (id: string): Effect.Effect<boolean> => fiberManager.isRunning(id)
|
|
241
|
+
|
|
242
|
+
return { start, stop, updateStubs, updateProxyConfig, isRunning } satisfies ImposterServerShape
|
|
243
|
+
})
|
|
244
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Config, Context, Layer } from "effect"
|
|
2
|
+
|
|
3
|
+
export interface AppConfigShape {
|
|
4
|
+
readonly adminPort: number
|
|
5
|
+
readonly portRangeMin: number
|
|
6
|
+
readonly portRangeMax: number
|
|
7
|
+
readonly maxImposters: number
|
|
8
|
+
readonly logLevel: "debug" | "info" | "warn" | "error"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class AppConfig extends Context.Tag("AppConfig")<AppConfig, AppConfigShape>() {}
|
|
12
|
+
|
|
13
|
+
const config = Config.all({
|
|
14
|
+
adminPort: Config.number("ADMIN_PORT").pipe(Config.withDefault(2525)),
|
|
15
|
+
portRangeMin: Config.number("PORT_RANGE_MIN").pipe(Config.withDefault(3000)),
|
|
16
|
+
portRangeMax: Config.number("PORT_RANGE_MAX").pipe(Config.withDefault(4000)),
|
|
17
|
+
maxImposters: Config.number("MAX_IMPOSTERS").pipe(Config.withDefault(100)),
|
|
18
|
+
logLevel: Config.literal("debug", "info", "warn", "error")("LOG_LEVEL")
|
|
19
|
+
.pipe(Config.withDefault("info" as const))
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
export const AppConfigLive = Layer.effect(AppConfig, config)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { Context, Effect, HashMap, Layer, Ref } from "effect"
|
|
2
|
+
import * as DateTime from "effect/DateTime"
|
|
3
|
+
import type { RequestLogEntry } from "../schemas/RequestLogSchema"
|
|
4
|
+
|
|
5
|
+
const BUFFER_SIZE = 1000
|
|
6
|
+
|
|
7
|
+
interface ImposterMetrics {
|
|
8
|
+
totalRequests: number
|
|
9
|
+
requestsByMethod: Record<string, number>
|
|
10
|
+
requestsByStatusCode: Record<string, number>
|
|
11
|
+
responseTimes: Float64Array
|
|
12
|
+
responseTimeIndex: number
|
|
13
|
+
responseTimeCount: number
|
|
14
|
+
firstRequestAt: DateTime.Utc
|
|
15
|
+
lastRequestAt: DateTime.Utc
|
|
16
|
+
errorCount: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Statistics {
|
|
20
|
+
readonly totalRequests: number
|
|
21
|
+
readonly requestsPerMinute: number
|
|
22
|
+
readonly averageResponseTime: number
|
|
23
|
+
readonly errorRate: number
|
|
24
|
+
readonly requestsByMethod: Record<string, number>
|
|
25
|
+
readonly requestsByStatusCode: Record<string, number>
|
|
26
|
+
readonly lastRequestAt?: DateTime.Utc
|
|
27
|
+
readonly p50ResponseTime?: number
|
|
28
|
+
readonly p95ResponseTime?: number
|
|
29
|
+
readonly p99ResponseTime?: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const makeEmptyMetrics = (now: DateTime.Utc): ImposterMetrics => ({
|
|
33
|
+
totalRequests: 0,
|
|
34
|
+
requestsByMethod: {},
|
|
35
|
+
requestsByStatusCode: {},
|
|
36
|
+
responseTimes: new Float64Array(BUFFER_SIZE),
|
|
37
|
+
responseTimeIndex: 0,
|
|
38
|
+
responseTimeCount: 0,
|
|
39
|
+
firstRequestAt: now,
|
|
40
|
+
lastRequestAt: now,
|
|
41
|
+
errorCount: 0
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const computePercentile = (sorted: Array<number>, p: number): number => {
|
|
45
|
+
if (sorted.length === 0) return 0
|
|
46
|
+
const index = Math.ceil((p / 100) * sorted.length) - 1
|
|
47
|
+
return sorted[Math.max(0, index)]!
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const computeStats = (metrics: ImposterMetrics): Statistics => {
|
|
51
|
+
const count = metrics.responseTimeCount
|
|
52
|
+
const total = metrics.totalRequests
|
|
53
|
+
|
|
54
|
+
// Compute average response time
|
|
55
|
+
let sumRT = 0
|
|
56
|
+
for (let i = 0; i < Math.min(count, BUFFER_SIZE); i++) {
|
|
57
|
+
sumRT += metrics.responseTimes[i]!
|
|
58
|
+
}
|
|
59
|
+
const avgRT = count > 0 ? sumRT / Math.min(count, BUFFER_SIZE) : 0
|
|
60
|
+
|
|
61
|
+
// Compute requests per minute
|
|
62
|
+
const elapsedMs = DateTime.toEpochMillis(metrics.lastRequestAt) - DateTime.toEpochMillis(metrics.firstRequestAt)
|
|
63
|
+
const elapsedMinutes = elapsedMs / 60000
|
|
64
|
+
const rpm = elapsedMinutes > 0 ? total / elapsedMinutes : total
|
|
65
|
+
|
|
66
|
+
// Compute error rate
|
|
67
|
+
const errorRate = total > 0 ? metrics.errorCount / total : 0
|
|
68
|
+
|
|
69
|
+
// Compute percentiles
|
|
70
|
+
const bufferLen = Math.min(count, BUFFER_SIZE)
|
|
71
|
+
const sorted = Array.from(metrics.responseTimes.subarray(0, bufferLen)).sort((a, b) => a - b)
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
totalRequests: total,
|
|
75
|
+
requestsPerMinute: Math.round(rpm * 100) / 100,
|
|
76
|
+
averageResponseTime: Math.round(avgRT * 100) / 100,
|
|
77
|
+
errorRate: Math.round(errorRate * 10000) / 10000,
|
|
78
|
+
requestsByMethod: { ...metrics.requestsByMethod },
|
|
79
|
+
requestsByStatusCode: { ...metrics.requestsByStatusCode },
|
|
80
|
+
...(total > 0 ? { lastRequestAt: metrics.lastRequestAt } : {}),
|
|
81
|
+
...(bufferLen > 0
|
|
82
|
+
? {
|
|
83
|
+
p50ResponseTime: computePercentile(sorted, 50),
|
|
84
|
+
p95ResponseTime: computePercentile(sorted, 95),
|
|
85
|
+
p99ResponseTime: computePercentile(sorted, 99)
|
|
86
|
+
}
|
|
87
|
+
: {})
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface MetricsServiceShape {
|
|
92
|
+
readonly recordRequest: (entry: RequestLogEntry) => Effect.Effect<void>
|
|
93
|
+
readonly getStats: (imposterId: string) => Effect.Effect<Statistics>
|
|
94
|
+
readonly resetStats: (imposterId: string) => Effect.Effect<void>
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export class MetricsService extends Context.Tag("MetricsService")<MetricsService, MetricsServiceShape>() {}
|
|
98
|
+
|
|
99
|
+
export const MetricsServiceLive = Layer.effect(
|
|
100
|
+
MetricsService,
|
|
101
|
+
Effect.gen(function*() {
|
|
102
|
+
const storeRef = yield* Ref.make(HashMap.empty<string, ImposterMetrics>())
|
|
103
|
+
|
|
104
|
+
const recordRequest = (entry: RequestLogEntry): Effect.Effect<void> =>
|
|
105
|
+
Ref.update(storeRef, (store) => {
|
|
106
|
+
const existing = HashMap.get(store, entry.imposterId)
|
|
107
|
+
const now = entry.timestamp
|
|
108
|
+
const metrics = existing._tag === "Some" ? existing.value : makeEmptyMetrics(now)
|
|
109
|
+
|
|
110
|
+
metrics.totalRequests += 1
|
|
111
|
+
|
|
112
|
+
// Method counts
|
|
113
|
+
const method = entry.request.method.toUpperCase()
|
|
114
|
+
metrics.requestsByMethod[method] = (metrics.requestsByMethod[method] ?? 0) + 1
|
|
115
|
+
|
|
116
|
+
// Status code counts
|
|
117
|
+
const statusKey = String(entry.response.status)
|
|
118
|
+
metrics.requestsByStatusCode[statusKey] = (metrics.requestsByStatusCode[statusKey] ?? 0) + 1
|
|
119
|
+
|
|
120
|
+
// Response time circular buffer
|
|
121
|
+
metrics.responseTimes[metrics.responseTimeIndex % BUFFER_SIZE] = entry.duration
|
|
122
|
+
metrics.responseTimeIndex = (metrics.responseTimeIndex + 1) % BUFFER_SIZE
|
|
123
|
+
metrics.responseTimeCount += 1
|
|
124
|
+
|
|
125
|
+
// Error tracking (4xx + 5xx)
|
|
126
|
+
if (entry.response.status >= 400) {
|
|
127
|
+
metrics.errorCount += 1
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
metrics.lastRequestAt = now
|
|
131
|
+
|
|
132
|
+
return HashMap.set(store, entry.imposterId, metrics)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
const getStats = (imposterId: string): Effect.Effect<Statistics> =>
|
|
136
|
+
Ref.get(storeRef).pipe(
|
|
137
|
+
Effect.map((store) => {
|
|
138
|
+
const existing = HashMap.get(store, imposterId)
|
|
139
|
+
if (existing._tag === "None") {
|
|
140
|
+
return {
|
|
141
|
+
totalRequests: 0,
|
|
142
|
+
requestsPerMinute: 0,
|
|
143
|
+
averageResponseTime: 0,
|
|
144
|
+
errorRate: 0,
|
|
145
|
+
requestsByMethod: {},
|
|
146
|
+
requestsByStatusCode: {}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return computeStats(existing.value)
|
|
150
|
+
})
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
const resetStats = (imposterId: string): Effect.Effect<void> => Ref.update(storeRef, HashMap.remove(imposterId))
|
|
154
|
+
|
|
155
|
+
return { recordRequest, getStats, resetStats } satisfies MetricsServiceShape
|
|
156
|
+
})
|
|
157
|
+
)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Context, Data, Effect, HashSet, Layer, Ref } from "effect"
|
|
2
|
+
import { AppConfig } from "./AppConfig"
|
|
3
|
+
|
|
4
|
+
export class PortAllocatorError extends Data.TaggedError("PortAllocatorError")<{
|
|
5
|
+
readonly reason: string
|
|
6
|
+
readonly port?: number
|
|
7
|
+
}> {}
|
|
8
|
+
|
|
9
|
+
export class PortExhaustedError extends Data.TaggedError("PortExhaustedError")<{
|
|
10
|
+
readonly rangeMin: number
|
|
11
|
+
readonly rangeMax: number
|
|
12
|
+
}> {}
|
|
13
|
+
|
|
14
|
+
export interface PortAllocatorShape {
|
|
15
|
+
readonly allocate: (preferred?: number) => Effect.Effect<number, PortAllocatorError | PortExhaustedError>
|
|
16
|
+
readonly release: (port: number) => Effect.Effect<void>
|
|
17
|
+
readonly isAvailable: (port: number) => Effect.Effect<boolean>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class PortAllocator extends Context.Tag("PortAllocator")<PortAllocator, PortAllocatorShape>() {}
|
|
21
|
+
|
|
22
|
+
export const PortAllocatorLive = Layer.effect(
|
|
23
|
+
PortAllocator,
|
|
24
|
+
Effect.gen(function*() {
|
|
25
|
+
const config = yield* AppConfig
|
|
26
|
+
const portsRef = yield* Ref.make(HashSet.empty<number>())
|
|
27
|
+
|
|
28
|
+
type AllocateResult = readonly [
|
|
29
|
+
Effect.Effect<number, PortAllocatorError | PortExhaustedError>,
|
|
30
|
+
HashSet.HashSet<number>
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
const allocate = (preferred?: number): Effect.Effect<number, PortAllocatorError | PortExhaustedError> => {
|
|
34
|
+
if (preferred !== undefined) {
|
|
35
|
+
return Ref.modify(portsRef, (ports): AllocateResult => {
|
|
36
|
+
if (HashSet.has(ports, preferred)) {
|
|
37
|
+
return [
|
|
38
|
+
Effect.fail(
|
|
39
|
+
new PortAllocatorError({ reason: `Port ${preferred} is already allocated`, port: preferred })
|
|
40
|
+
),
|
|
41
|
+
ports
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
return [Effect.succeed(preferred), HashSet.add(ports, preferred)]
|
|
45
|
+
}).pipe(Effect.flatten)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return Ref.modify(portsRef, (ports): AllocateResult => {
|
|
49
|
+
for (let port = config.portRangeMin; port <= config.portRangeMax; port++) {
|
|
50
|
+
if (!HashSet.has(ports, port)) {
|
|
51
|
+
return [Effect.succeed(port), HashSet.add(ports, port)]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return [
|
|
55
|
+
Effect.fail(new PortExhaustedError({ rangeMin: config.portRangeMin, rangeMax: config.portRangeMax })),
|
|
56
|
+
ports
|
|
57
|
+
]
|
|
58
|
+
}).pipe(Effect.flatten)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const release = (port: number): Effect.Effect<void> => Ref.update(portsRef, HashSet.remove(port))
|
|
62
|
+
|
|
63
|
+
const isAvailable = (port: number): Effect.Effect<boolean> =>
|
|
64
|
+
Ref.get(portsRef).pipe(Effect.map((ports) => !HashSet.has(ports, port)))
|
|
65
|
+
|
|
66
|
+
return { allocate, release, isAvailable }
|
|
67
|
+
})
|
|
68
|
+
)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Context, Data, Effect, Layer } from "effect"
|
|
2
|
+
import type { ProxyConfigDomain } from "../domain/imposter"
|
|
3
|
+
import type { RequestContext } from "../matching/RequestMatcher"
|
|
4
|
+
import { NonEmptyString } from "../schemas/common"
|
|
5
|
+
import type { Stub } from "../schemas/StubSchema"
|
|
6
|
+
import { Uuid } from "./Uuid"
|
|
7
|
+
|
|
8
|
+
export class ProxyError extends Data.TaggedError("ProxyError")<{
|
|
9
|
+
readonly targetUrl: string
|
|
10
|
+
readonly reason: string
|
|
11
|
+
readonly cause?: unknown
|
|
12
|
+
}> {}
|
|
13
|
+
|
|
14
|
+
const HOP_BY_HOP_HEADERS = new Set([
|
|
15
|
+
"host",
|
|
16
|
+
"connection",
|
|
17
|
+
"keep-alive",
|
|
18
|
+
"transfer-encoding",
|
|
19
|
+
"upgrade",
|
|
20
|
+
"proxy-authenticate",
|
|
21
|
+
"proxy-authorization",
|
|
22
|
+
"te",
|
|
23
|
+
"trailers"
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
export interface ProxyServiceShape {
|
|
27
|
+
readonly forward: (
|
|
28
|
+
ctx: RequestContext,
|
|
29
|
+
config: ProxyConfigDomain,
|
|
30
|
+
originalUrl: URL
|
|
31
|
+
) => Effect.Effect<Response, ProxyError>
|
|
32
|
+
readonly recordAsStub: (
|
|
33
|
+
request: RequestContext,
|
|
34
|
+
response: Response
|
|
35
|
+
) => Effect.Effect<Stub>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class ProxyService extends Context.Tag("ProxyService")<ProxyService, ProxyServiceShape>() {}
|
|
39
|
+
|
|
40
|
+
export const ProxyServiceLive = Layer.effect(
|
|
41
|
+
ProxyService,
|
|
42
|
+
Effect.gen(function*() {
|
|
43
|
+
const uuid = yield* Uuid
|
|
44
|
+
|
|
45
|
+
const forward = (
|
|
46
|
+
ctx: RequestContext,
|
|
47
|
+
config: ProxyConfigDomain,
|
|
48
|
+
originalUrl: URL
|
|
49
|
+
): Effect.Effect<Response, ProxyError> =>
|
|
50
|
+
Effect.gen(function*() {
|
|
51
|
+
// Build target URL preserving path and query
|
|
52
|
+
const targetBase = config.targetUrl.replace(/\/$/, "")
|
|
53
|
+
const targetUrl = `${targetBase}${originalUrl.pathname}${originalUrl.search}`
|
|
54
|
+
|
|
55
|
+
// Build headers
|
|
56
|
+
const headers = new Headers()
|
|
57
|
+
for (const [key, val] of Object.entries(ctx.headers)) {
|
|
58
|
+
if (!HOP_BY_HOP_HEADERS.has(key.toLowerCase())) {
|
|
59
|
+
headers.set(key, val)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Remove headers specified in config
|
|
64
|
+
for (const h of config.removeHeaders) {
|
|
65
|
+
headers.delete(h)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Add headers specified in config
|
|
69
|
+
if (config.addHeaders) {
|
|
70
|
+
for (const [key, val] of Object.entries(config.addHeaders)) {
|
|
71
|
+
headers.set(key, val)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Build body
|
|
76
|
+
let body: string | undefined
|
|
77
|
+
if (ctx.body !== undefined && ctx.body !== null) {
|
|
78
|
+
body = typeof ctx.body === "string" ? ctx.body : JSON.stringify(ctx.body)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const response = yield* Effect.tryPromise({
|
|
82
|
+
try: (signal) =>
|
|
83
|
+
fetch(targetUrl, {
|
|
84
|
+
method: ctx.method,
|
|
85
|
+
headers,
|
|
86
|
+
...(body !== undefined && ctx.method !== "GET" && ctx.method !== "HEAD" ? { body } : {}),
|
|
87
|
+
redirect: config.followRedirects ? "follow" : "manual",
|
|
88
|
+
signal
|
|
89
|
+
}),
|
|
90
|
+
catch: (err) => new ProxyError({ targetUrl, reason: `Failed to reach target: ${err}`, cause: err })
|
|
91
|
+
}).pipe(Effect.timeoutFail({
|
|
92
|
+
duration: `${config.timeout} millis`,
|
|
93
|
+
onTimeout: () => new ProxyError({ targetUrl, reason: `Request timed out after ${config.timeout}ms` })
|
|
94
|
+
}))
|
|
95
|
+
|
|
96
|
+
return response
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const recordAsStub = (
|
|
100
|
+
request: RequestContext,
|
|
101
|
+
response: Response
|
|
102
|
+
): Effect.Effect<Stub> =>
|
|
103
|
+
Effect.gen(function*() {
|
|
104
|
+
const id = yield* uuid.generateShort
|
|
105
|
+
|
|
106
|
+
const respHeaders: Record<string, string> = {}
|
|
107
|
+
response.headers.forEach((val, key) => {
|
|
108
|
+
respHeaders[key] = val
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const respText = yield* Effect.promise(() => response.text())
|
|
112
|
+
let respBody: unknown = respText
|
|
113
|
+
const contentType = response.headers.get("content-type") ?? ""
|
|
114
|
+
if (contentType.includes("application/json") && respText) {
|
|
115
|
+
try {
|
|
116
|
+
respBody = JSON.parse(respText)
|
|
117
|
+
} catch {
|
|
118
|
+
// keep as string
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
id: NonEmptyString.make(id),
|
|
124
|
+
predicates: [
|
|
125
|
+
{ field: "method" as const, operator: "equals" as const, value: request.method, caseSensitive: true },
|
|
126
|
+
{ field: "path" as const, operator: "equals" as const, value: request.path, caseSensitive: true }
|
|
127
|
+
],
|
|
128
|
+
responses: [{
|
|
129
|
+
status: response.status,
|
|
130
|
+
headers: respHeaders,
|
|
131
|
+
body: respBody
|
|
132
|
+
}],
|
|
133
|
+
responseMode: "sequential" as const
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
return { forward, recordAsStub } satisfies ProxyServiceShape
|
|
138
|
+
})
|
|
139
|
+
)
|