threadforge 0.1.1 → 0.2.1
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/README.md +52 -20
- package/bin/forge.js +2 -1058
- package/bin/host-commands.d.ts +2 -0
- package/bin/host-commands.d.ts.map +1 -0
- package/bin/host-commands.js +7 -8
- package/bin/platform-commands.d.ts +2 -0
- package/bin/platform-commands.d.ts.map +1 -0
- package/bin/platform-commands.js +118 -36
- package/dist/cli/base-command.d.ts +12 -0
- package/dist/cli/base-command.d.ts.map +1 -0
- package/dist/cli/base-command.js +25 -0
- package/dist/cli/base-command.js.map +1 -0
- package/dist/cli/commands/build.d.ts +10 -0
- package/dist/cli/commands/build.d.ts.map +1 -0
- package/dist/cli/commands/build.js +110 -0
- package/dist/cli/commands/build.js.map +1 -0
- package/dist/cli/commands/deploy.d.ts +12 -0
- package/dist/cli/commands/deploy.d.ts.map +1 -0
- package/dist/cli/commands/deploy.js +143 -0
- package/dist/cli/commands/deploy.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +10 -0
- package/dist/cli/commands/dev.d.ts.map +1 -0
- package/dist/cli/commands/dev.js +138 -0
- package/dist/cli/commands/dev.js.map +1 -0
- package/dist/cli/commands/generate.d.ts +10 -0
- package/dist/cli/commands/generate.d.ts.map +1 -0
- package/dist/cli/commands/generate.js +76 -0
- package/dist/cli/commands/generate.js.map +1 -0
- package/dist/cli/commands/host.d.ts +8 -0
- package/dist/cli/commands/host.d.ts.map +1 -0
- package/dist/cli/commands/host.js +20 -0
- package/dist/cli/commands/host.js.map +1 -0
- package/dist/cli/commands/init.d.ts +16 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +246 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/platform.d.ts +8 -0
- package/dist/cli/commands/platform.d.ts.map +1 -0
- package/dist/cli/commands/platform.js +20 -0
- package/dist/cli/commands/platform.js.map +1 -0
- package/dist/cli/commands/restart.d.ts +8 -0
- package/dist/cli/commands/restart.d.ts.map +1 -0
- package/dist/cli/commands/restart.js +13 -0
- package/dist/cli/commands/restart.js.map +1 -0
- package/dist/cli/commands/scaffold/frontend.d.ts +10 -0
- package/dist/cli/commands/scaffold/frontend.d.ts.map +1 -0
- package/dist/cli/commands/scaffold/frontend.js +130 -0
- package/dist/cli/commands/scaffold/frontend.js.map +1 -0
- package/dist/cli/commands/scaffold/react.d.ts +7 -0
- package/dist/cli/commands/scaffold/react.d.ts.map +1 -0
- package/dist/cli/commands/scaffold/react.js +12 -0
- package/dist/cli/commands/scaffold/react.js.map +1 -0
- package/dist/cli/commands/scale.d.ts +8 -0
- package/dist/cli/commands/scale.d.ts.map +1 -0
- package/dist/cli/commands/scale.js +13 -0
- package/dist/cli/commands/scale.js.map +1 -0
- package/dist/cli/commands/start.d.ts +10 -0
- package/dist/cli/commands/start.d.ts.map +1 -0
- package/dist/cli/commands/start.js +71 -0
- package/dist/cli/commands/start.js.map +1 -0
- package/dist/cli/commands/status.d.ts +11 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +60 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/stop.d.ts +10 -0
- package/dist/cli/commands/stop.d.ts.map +1 -0
- package/dist/cli/commands/stop.js +89 -0
- package/dist/cli/commands/stop.js.map +1 -0
- package/dist/cli/util/config-discovery.d.ts +8 -0
- package/dist/cli/util/config-discovery.d.ts.map +1 -0
- package/dist/cli/util/config-discovery.js +70 -0
- package/dist/cli/util/config-discovery.js.map +1 -0
- package/dist/cli/util/config-patcher.d.ts +17 -0
- package/dist/cli/util/config-patcher.d.ts.map +1 -0
- package/dist/cli/util/config-patcher.js +439 -0
- package/dist/cli/util/config-patcher.js.map +1 -0
- package/dist/cli/util/frontend-dev.d.ts +8 -0
- package/dist/cli/util/frontend-dev.d.ts.map +1 -0
- package/dist/cli/util/frontend-dev.js +117 -0
- package/dist/cli/util/frontend-dev.js.map +1 -0
- package/dist/cli/util/process.d.ts +5 -0
- package/dist/cli/util/process.d.ts.map +1 -0
- package/dist/cli/util/process.js +17 -0
- package/dist/cli/util/process.js.map +1 -0
- package/dist/cli/util/templates.d.ts +10 -0
- package/dist/cli/util/templates.d.ts.map +1 -0
- package/dist/cli/util/templates.js +157 -0
- package/dist/cli/util/templates.js.map +1 -0
- package/dist/core/AlertSink.d.ts +83 -0
- package/dist/core/AlertSink.d.ts.map +1 -0
- package/dist/core/AlertSink.js +126 -0
- package/dist/core/AlertSink.js.map +1 -0
- package/dist/core/DirectMessageBus.d.ts +88 -0
- package/dist/core/DirectMessageBus.d.ts.map +1 -0
- package/dist/core/DirectMessageBus.js +352 -0
- package/dist/core/DirectMessageBus.js.map +1 -0
- package/dist/core/EndpointResolver.d.ts +111 -0
- package/dist/core/EndpointResolver.d.ts.map +1 -0
- package/dist/core/EndpointResolver.js +336 -0
- package/dist/core/EndpointResolver.js.map +1 -0
- package/dist/core/ForgeContext.d.ts +221 -0
- package/dist/core/ForgeContext.d.ts.map +1 -0
- package/dist/core/ForgeContext.js +1169 -0
- package/dist/core/ForgeContext.js.map +1 -0
- package/dist/core/ForgeEndpoints.d.ts +71 -0
- package/dist/core/ForgeEndpoints.d.ts.map +1 -0
- package/dist/core/ForgeEndpoints.js +442 -0
- package/dist/core/ForgeEndpoints.js.map +1 -0
- package/dist/core/ForgeHost.d.ts +82 -0
- package/dist/core/ForgeHost.d.ts.map +1 -0
- package/dist/core/ForgeHost.js +107 -0
- package/dist/core/ForgeHost.js.map +1 -0
- package/dist/core/ForgePlatform.d.ts +96 -0
- package/dist/core/ForgePlatform.d.ts.map +1 -0
- package/dist/core/ForgePlatform.js +136 -0
- package/dist/core/ForgePlatform.js.map +1 -0
- package/dist/core/ForgeWebSocket.d.ts +56 -0
- package/dist/core/ForgeWebSocket.d.ts.map +1 -0
- package/dist/core/ForgeWebSocket.js +415 -0
- package/dist/core/ForgeWebSocket.js.map +1 -0
- package/dist/core/Ingress.d.ts +329 -0
- package/dist/core/Ingress.d.ts.map +1 -0
- package/dist/core/Ingress.js +694 -0
- package/dist/core/Ingress.js.map +1 -0
- package/dist/core/Interceptors.d.ts +134 -0
- package/dist/core/Interceptors.d.ts.map +1 -0
- package/dist/core/Interceptors.js +416 -0
- package/dist/core/Interceptors.js.map +1 -0
- package/dist/core/Logger.d.ts +20 -0
- package/dist/core/Logger.d.ts.map +1 -0
- package/dist/core/Logger.js +77 -0
- package/dist/core/Logger.js.map +1 -0
- package/dist/core/MessageBus.d.ts +15 -0
- package/dist/core/MessageBus.d.ts.map +1 -0
- package/dist/core/MessageBus.js +18 -0
- package/dist/core/MessageBus.js.map +1 -0
- package/dist/core/Prometheus.d.ts +80 -0
- package/dist/core/Prometheus.d.ts.map +1 -0
- package/dist/core/Prometheus.js +332 -0
- package/dist/core/Prometheus.js.map +1 -0
- package/dist/core/RequestContext.d.ts +214 -0
- package/dist/core/RequestContext.d.ts.map +1 -0
- package/dist/core/RequestContext.js +556 -0
- package/dist/core/RequestContext.js.map +1 -0
- package/dist/core/Router.d.ts +45 -0
- package/dist/core/Router.d.ts.map +1 -0
- package/dist/core/Router.js +285 -0
- package/dist/core/Router.js.map +1 -0
- package/dist/core/RoutingStrategy.d.ts +116 -0
- package/dist/core/RoutingStrategy.d.ts.map +1 -0
- package/dist/core/RoutingStrategy.js +306 -0
- package/dist/core/RoutingStrategy.js.map +1 -0
- package/dist/core/RpcConfig.d.ts +72 -0
- package/dist/core/RpcConfig.d.ts.map +1 -0
- package/dist/core/RpcConfig.js +127 -0
- package/dist/core/RpcConfig.js.map +1 -0
- package/dist/core/SignatureCache.d.ts +81 -0
- package/dist/core/SignatureCache.d.ts.map +1 -0
- package/dist/core/SignatureCache.js +172 -0
- package/dist/core/SignatureCache.js.map +1 -0
- package/dist/core/StaticFileServer.d.ts +34 -0
- package/dist/core/StaticFileServer.d.ts.map +1 -0
- package/dist/core/StaticFileServer.js +497 -0
- package/dist/core/StaticFileServer.js.map +1 -0
- package/dist/core/Supervisor.d.ts +198 -0
- package/dist/core/Supervisor.d.ts.map +1 -0
- package/dist/core/Supervisor.js +1418 -0
- package/dist/core/Supervisor.js.map +1 -0
- package/dist/core/ThreadAllocator.d.ts +52 -0
- package/dist/core/ThreadAllocator.d.ts.map +1 -0
- package/dist/core/ThreadAllocator.js +174 -0
- package/dist/core/ThreadAllocator.js.map +1 -0
- package/dist/core/WorkerChannelManager.d.ts +130 -0
- package/dist/core/WorkerChannelManager.d.ts.map +1 -0
- package/dist/core/WorkerChannelManager.js +956 -0
- package/dist/core/WorkerChannelManager.js.map +1 -0
- package/dist/core/config-enums.d.ts +41 -0
- package/dist/core/config-enums.d.ts.map +1 -0
- package/dist/core/config-enums.js +59 -0
- package/dist/core/config-enums.js.map +1 -0
- package/dist/core/config.d.ts +159 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +694 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/host-config.d.ts +146 -0
- package/dist/core/host-config.d.ts.map +1 -0
- package/dist/core/host-config.js +312 -0
- package/dist/core/host-config.js.map +1 -0
- package/dist/core/ipc-errors.d.ts +27 -0
- package/dist/core/ipc-errors.d.ts.map +1 -0
- package/dist/core/ipc-errors.js +36 -0
- package/dist/core/ipc-errors.js.map +1 -0
- package/dist/core/network-utils.d.ts +35 -0
- package/dist/core/network-utils.d.ts.map +1 -0
- package/dist/core/network-utils.js +145 -0
- package/dist/core/network-utils.js.map +1 -0
- package/dist/core/platform-config.d.ts +142 -0
- package/dist/core/platform-config.d.ts.map +1 -0
- package/dist/core/platform-config.js +299 -0
- package/dist/core/platform-config.js.map +1 -0
- package/dist/decorators/ServiceProxy.d.ts +175 -0
- package/dist/decorators/ServiceProxy.d.ts.map +1 -0
- package/dist/decorators/ServiceProxy.js +969 -0
- package/dist/decorators/ServiceProxy.js.map +1 -0
- package/dist/decorators/index.d.ts +146 -0
- package/dist/decorators/index.d.ts.map +1 -0
- package/dist/decorators/index.js +545 -0
- package/dist/decorators/index.js.map +1 -0
- package/dist/deploy/NginxGenerator.d.ts +165 -0
- package/dist/deploy/NginxGenerator.d.ts.map +1 -0
- package/dist/deploy/NginxGenerator.js +781 -0
- package/dist/deploy/NginxGenerator.js.map +1 -0
- package/dist/deploy/PlatformManifestGenerator.d.ts +43 -0
- package/dist/deploy/PlatformManifestGenerator.d.ts.map +1 -0
- package/dist/deploy/PlatformManifestGenerator.js +80 -0
- package/dist/deploy/PlatformManifestGenerator.js.map +1 -0
- package/dist/deploy/RouteManifestGenerator.d.ts +42 -0
- package/dist/deploy/RouteManifestGenerator.d.ts.map +1 -0
- package/dist/deploy/RouteManifestGenerator.js +105 -0
- package/dist/deploy/RouteManifestGenerator.js.map +1 -0
- package/dist/deploy/index.d.ts +210 -0
- package/dist/deploy/index.d.ts.map +1 -0
- package/dist/deploy/index.js +918 -0
- package/dist/deploy/index.js.map +1 -0
- package/dist/frontend/FrontendDevLifecycle.d.ts +26 -0
- package/dist/frontend/FrontendDevLifecycle.d.ts.map +1 -0
- package/dist/frontend/FrontendDevLifecycle.js +60 -0
- package/dist/frontend/FrontendDevLifecycle.js.map +1 -0
- package/dist/frontend/FrontendPluginOrchestrator.d.ts +64 -0
- package/dist/frontend/FrontendPluginOrchestrator.d.ts.map +1 -0
- package/dist/frontend/FrontendPluginOrchestrator.js +167 -0
- package/dist/frontend/FrontendPluginOrchestrator.js.map +1 -0
- package/dist/frontend/SiteResolver.d.ts +33 -0
- package/dist/frontend/SiteResolver.d.ts.map +1 -0
- package/dist/frontend/SiteResolver.js +53 -0
- package/dist/frontend/SiteResolver.js.map +1 -0
- package/dist/frontend/StaticMountRegistry.d.ts +36 -0
- package/dist/frontend/StaticMountRegistry.d.ts.map +1 -0
- package/dist/frontend/StaticMountRegistry.js +94 -0
- package/dist/frontend/StaticMountRegistry.js.map +1 -0
- package/dist/frontend/index.d.ts +7 -0
- package/dist/frontend/index.d.ts.map +1 -0
- package/{src → dist}/frontend/index.js +4 -2
- package/dist/frontend/index.js.map +1 -0
- package/dist/frontend/pathUtils.d.ts +8 -0
- package/dist/frontend/pathUtils.d.ts.map +1 -0
- package/dist/frontend/pathUtils.js +17 -0
- package/dist/frontend/pathUtils.js.map +1 -0
- package/dist/frontend/plugins/index.d.ts +2 -0
- package/dist/frontend/plugins/index.d.ts.map +1 -0
- package/{src → dist}/frontend/plugins/index.js +1 -1
- package/dist/frontend/plugins/index.js.map +1 -0
- package/dist/frontend/plugins/viteFrontend.d.ts +51 -0
- package/dist/frontend/plugins/viteFrontend.d.ts.map +1 -0
- package/dist/frontend/plugins/viteFrontend.js +134 -0
- package/dist/frontend/plugins/viteFrontend.js.map +1 -0
- package/dist/frontend/types.d.ts +25 -0
- package/dist/frontend/types.d.ts.map +1 -0
- package/dist/frontend/types.js +2 -0
- package/dist/frontend/types.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/internals.d.ts +21 -0
- package/dist/internals.d.ts.map +1 -0
- package/{src → dist}/internals.js +12 -14
- package/dist/internals.js.map +1 -0
- package/dist/plugins/PluginManager.d.ts +209 -0
- package/dist/plugins/PluginManager.d.ts.map +1 -0
- package/dist/plugins/PluginManager.js +365 -0
- package/dist/plugins/PluginManager.js.map +1 -0
- package/dist/plugins/ScopedPostgres.d.ts +78 -0
- package/dist/plugins/ScopedPostgres.d.ts.map +1 -0
- package/dist/plugins/ScopedPostgres.js +190 -0
- package/dist/plugins/ScopedPostgres.js.map +1 -0
- package/dist/plugins/ScopedRedis.d.ts +88 -0
- package/dist/plugins/ScopedRedis.d.ts.map +1 -0
- package/dist/plugins/ScopedRedis.js +169 -0
- package/dist/plugins/ScopedRedis.js.map +1 -0
- package/dist/plugins/index.d.ts +289 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/index.js +1942 -0
- package/dist/plugins/index.js.map +1 -0
- package/dist/plugins/types.d.ts +59 -0
- package/dist/plugins/types.d.ts.map +1 -0
- package/dist/plugins/types.js +2 -0
- package/dist/plugins/types.js.map +1 -0
- package/dist/registry/ServiceRegistry.d.ts +305 -0
- package/dist/registry/ServiceRegistry.d.ts.map +1 -0
- package/dist/registry/ServiceRegistry.js +735 -0
- package/dist/registry/ServiceRegistry.js.map +1 -0
- package/dist/scaling/ScaleAdvisor.d.ts +214 -0
- package/dist/scaling/ScaleAdvisor.d.ts.map +1 -0
- package/dist/scaling/ScaleAdvisor.js +526 -0
- package/dist/scaling/ScaleAdvisor.js.map +1 -0
- package/dist/services/Service.d.ts +164 -0
- package/dist/services/Service.d.ts.map +1 -0
- package/dist/services/Service.js +106 -0
- package/dist/services/Service.js.map +1 -0
- package/dist/services/worker-bootstrap.d.ts +15 -0
- package/dist/services/worker-bootstrap.d.ts.map +1 -0
- package/dist/services/worker-bootstrap.js +744 -0
- package/dist/services/worker-bootstrap.js.map +1 -0
- package/dist/templates/auth-service.d.ts +42 -0
- package/dist/templates/auth-service.d.ts.map +1 -0
- package/dist/templates/auth-service.js +54 -0
- package/dist/templates/auth-service.js.map +1 -0
- package/dist/templates/identity-service.d.ts +50 -0
- package/dist/templates/identity-service.d.ts.map +1 -0
- package/dist/templates/identity-service.js +62 -0
- package/dist/templates/identity-service.js.map +1 -0
- package/dist/types/contract.d.ts +120 -0
- package/dist/types/contract.d.ts.map +1 -0
- package/dist/types/contract.js +69 -0
- package/dist/types/contract.js.map +1 -0
- package/package.json +78 -20
- package/src/core/DirectMessageBus.js +0 -364
- package/src/core/EndpointResolver.js +0 -259
- package/src/core/ForgeContext.js +0 -2236
- package/src/core/ForgeHost.js +0 -122
- package/src/core/ForgePlatform.js +0 -145
- package/src/core/Ingress.js +0 -768
- package/src/core/Interceptors.js +0 -420
- package/src/core/MessageBus.js +0 -321
- package/src/core/Prometheus.js +0 -305
- package/src/core/RequestContext.js +0 -413
- package/src/core/RoutingStrategy.js +0 -330
- package/src/core/Supervisor.js +0 -1349
- package/src/core/ThreadAllocator.js +0 -196
- package/src/core/WorkerChannelManager.js +0 -879
- package/src/core/config.js +0 -637
- package/src/core/host-config.js +0 -311
- package/src/core/network-utils.js +0 -166
- package/src/core/platform-config.js +0 -308
- package/src/decorators/ServiceProxy.js +0 -904
- package/src/decorators/index.js +0 -571
- package/src/deploy/NginxGenerator.js +0 -865
- package/src/deploy/PlatformManifestGenerator.js +0 -96
- package/src/deploy/RouteManifestGenerator.js +0 -112
- package/src/deploy/index.js +0 -984
- package/src/frontend/FrontendDevLifecycle.js +0 -65
- package/src/frontend/FrontendPluginOrchestrator.js +0 -187
- package/src/frontend/SiteResolver.js +0 -63
- package/src/frontend/StaticMountRegistry.js +0 -90
- package/src/frontend/plugins/viteFrontend.js +0 -79
- package/src/frontend/types.js +0 -35
- package/src/index.js +0 -58
- package/src/plugins/PluginManager.js +0 -537
- package/src/plugins/ScopedPostgres.js +0 -192
- package/src/plugins/ScopedRedis.js +0 -142
- package/src/plugins/index.js +0 -1756
- package/src/registry/ServiceRegistry.js +0 -797
- package/src/scaling/ScaleAdvisor.js +0 -442
- package/src/services/Service.js +0 -195
- package/src/services/worker-bootstrap.js +0 -679
- package/src/templates/auth-service.js +0 -65
- package/src/templates/identity-service.js +0 -75
package/src/core/Ingress.js
DELETED
|
@@ -1,768 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ingress Protection
|
|
3
|
-
*
|
|
4
|
-
* Multiple layers of defense between the internet and your services.
|
|
5
|
-
* Each layer catches different failure modes:
|
|
6
|
-
*
|
|
7
|
-
* ┌─────────────────────────────────────────────────────────────┐
|
|
8
|
-
* │ INTERNET │
|
|
9
|
-
* └───────────────────────────┬─────────────────────────────────┘
|
|
10
|
-
* ▼
|
|
11
|
-
* ┌─────────────────────────────────────────────────────────────┐
|
|
12
|
-
* │ Layer 1: CONNECTION LIMITER │
|
|
13
|
-
* │ Total open connections capped at N (e.g. 10,000). │
|
|
14
|
-
* │ New connections get TCP RST when full. │
|
|
15
|
-
* │ Prevents file descriptor exhaustion. │
|
|
16
|
-
* └───────────────────────────┬─────────────────────────────────┘
|
|
17
|
-
* ▼
|
|
18
|
-
* ┌─────────────────────────────────────────────────────────────┐
|
|
19
|
-
* │ Layer 2: RATE LIMITER (per client) │
|
|
20
|
-
* │ Token bucket per IP / API key. Burst-friendly. │
|
|
21
|
-
* │ Returns 429 Too Many Requests when exceeded. │
|
|
22
|
-
* │ Prevents single client from consuming all capacity. │
|
|
23
|
-
* └───────────────────────────┬─────────────────────────────────┘
|
|
24
|
-
* ▼
|
|
25
|
-
* ┌─────────────────────────────────────────────────────────────┐
|
|
26
|
-
* │ Layer 3: GLOBAL LOAD SHEDDER │
|
|
27
|
-
* │ Monitors gateway CPU + event loop lag. │
|
|
28
|
-
* │ When overloaded, sheds low-priority requests (503). │
|
|
29
|
-
* │ High-priority requests (health checks, auth) pass through. │
|
|
30
|
-
* │ Prevents cascade failure in the gateway itself. │
|
|
31
|
-
* └───────────────────────────┬─────────────────────────────────┘
|
|
32
|
-
* ▼
|
|
33
|
-
* ┌─────────────────────────────────────────────────────────────┐
|
|
34
|
-
* │ Layer 4: ADAPTIVE CONCURRENCY (per service) │
|
|
35
|
-
* │ Limits in-flight requests to each downstream service. │
|
|
36
|
-
* │ Dynamically adjusts limit based on observed latency. │
|
|
37
|
-
* │ When a service slows down, we send FEWER requests to it. │
|
|
38
|
-
* │ Returns 503 when the service's concurrency window is full. │
|
|
39
|
-
* │ Prevents a slow service from consuming all gateway threads.│
|
|
40
|
-
* └───────────────────────────┬─────────────────────────────────┘
|
|
41
|
-
* ▼
|
|
42
|
-
* ┌─────────────────────────────────────────────────────────────┐
|
|
43
|
-
* │ SERVICE (users, billing, etc.) │
|
|
44
|
-
* └─────────────────────────────────────────────────────────────┘
|
|
45
|
-
*
|
|
46
|
-
* Usage in a gateway service:
|
|
47
|
-
*
|
|
48
|
-
* import { IngressProtection } from 'threadforge/ingress';
|
|
49
|
-
*
|
|
50
|
-
* export default class GatewayService extends Service {
|
|
51
|
-
* async onStart(ctx) {
|
|
52
|
-
* this.ingress = new IngressProtection({
|
|
53
|
-
* maxConnections: 10000,
|
|
54
|
-
* rateLimit: { windowMs: 60000, maxRequests: 100 },
|
|
55
|
-
* loadShedding: { eventLoopThresholdMs: 100 },
|
|
56
|
-
* services: {
|
|
57
|
-
* users: { maxConcurrent: 200 },
|
|
58
|
-
* billing: { maxConcurrent: 50 },
|
|
59
|
-
* notifications: { maxConcurrent: 100 },
|
|
60
|
-
* },
|
|
61
|
-
* });
|
|
62
|
-
*
|
|
63
|
-
* // Apply as middleware
|
|
64
|
-
* ctx.router.use(this.ingress.middleware());
|
|
65
|
-
* }
|
|
66
|
-
* }
|
|
67
|
-
*/
|
|
68
|
-
|
|
69
|
-
import { EventEmitter } from "node:events";
|
|
70
|
-
import { isPrivateNetwork } from "./ForgeContext.js";
|
|
71
|
-
|
|
72
|
-
// ─── Rate Limiter (Token Bucket) ────────────────────────────
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Per-client rate limiter using the token bucket algorithm.
|
|
76
|
-
*
|
|
77
|
-
* Token bucket is burst-friendly: a client that hasn't made
|
|
78
|
-
* requests in a while accumulates tokens and can burst.
|
|
79
|
-
* A client that's been hammering the API runs out of tokens
|
|
80
|
-
* and gets 429'd.
|
|
81
|
-
*
|
|
82
|
-
* Why token bucket instead of sliding window:
|
|
83
|
-
* - Allows natural burst patterns (page load = 10 requests at once)
|
|
84
|
-
* - Smoother than fixed windows (no "thundering herd" at window reset)
|
|
85
|
-
* - O(1) per request (no sorted sets or sliding counters)
|
|
86
|
-
*/
|
|
87
|
-
export class RateLimiter {
|
|
88
|
-
/**
|
|
89
|
-
* @param {Object} options
|
|
90
|
-
* @param {number} [options.maxTokens=100] - Max tokens per client (burst size)
|
|
91
|
-
* @param {number} [options.refillRate=10] - Tokens added per second
|
|
92
|
-
* @param {number} [options.windowMs=60000] - Cleanup interval for stale buckets
|
|
93
|
-
* @param {Function} [options.keyExtractor] - (req) => string, extracts client identity
|
|
94
|
-
*/
|
|
95
|
-
constructor(options = {}) {
|
|
96
|
-
this.maxTokens = options.maxTokens ?? 100;
|
|
97
|
-
this.refillRate = options.refillRate ?? (options.maxRequests ?? 100) / 60;
|
|
98
|
-
this.windowMs = options.windowMs ?? 60000;
|
|
99
|
-
this.keyExtractor = options.keyExtractor ?? defaultKeyExtractor;
|
|
100
|
-
|
|
101
|
-
/** @type {Map<string, {tokens: number, lastRefill: number}>} */
|
|
102
|
-
this.buckets = new Map();
|
|
103
|
-
|
|
104
|
-
// Periodically clean stale buckets
|
|
105
|
-
this._cleanupTimer = setInterval(() => this._cleanup(), this.windowMs);
|
|
106
|
-
this._cleanupTimer.unref();
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Check if a request is allowed.
|
|
111
|
-
* @returns {{ allowed: boolean, remaining: number, retryAfter?: number }}
|
|
112
|
-
*/
|
|
113
|
-
check(req) {
|
|
114
|
-
const key = this.keyExtractor(req);
|
|
115
|
-
const now = Date.now();
|
|
116
|
-
|
|
117
|
-
let bucket = this.buckets.get(key);
|
|
118
|
-
if (!bucket) {
|
|
119
|
-
bucket = { tokens: this.maxTokens, lastRefill: now };
|
|
120
|
-
this.buckets.set(key, bucket);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Refill tokens based on elapsed time
|
|
124
|
-
const elapsed = (now - bucket.lastRefill) / 1000;
|
|
125
|
-
bucket.tokens = Math.min(this.maxTokens, bucket.tokens + elapsed * this.refillRate);
|
|
126
|
-
bucket.lastRefill = now;
|
|
127
|
-
|
|
128
|
-
if (bucket.tokens >= 1) {
|
|
129
|
-
bucket.tokens -= 1;
|
|
130
|
-
return {
|
|
131
|
-
allowed: true,
|
|
132
|
-
remaining: Math.floor(bucket.tokens),
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Rate limited — calculate retry-after
|
|
137
|
-
const tokensNeeded = 1 - bucket.tokens;
|
|
138
|
-
const retryAfterMs = Math.ceil((tokensNeeded / this.refillRate) * 1000);
|
|
139
|
-
|
|
140
|
-
return {
|
|
141
|
-
allowed: false,
|
|
142
|
-
remaining: 0,
|
|
143
|
-
retryAfter: retryAfterMs,
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
_cleanup() {
|
|
148
|
-
const staleThreshold = Date.now() - this.windowMs * 2;
|
|
149
|
-
for (const [key, bucket] of this.buckets) {
|
|
150
|
-
if (bucket.lastRefill < staleThreshold) {
|
|
151
|
-
this.buckets.delete(key);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
get stats() {
|
|
157
|
-
return {
|
|
158
|
-
activeClients: this.buckets.size,
|
|
159
|
-
maxTokens: this.maxTokens,
|
|
160
|
-
refillRate: this.refillRate,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
stop() {
|
|
165
|
-
if (this._cleanupTimer) clearInterval(this._cleanupTimer);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function isValidIP(str) {
|
|
170
|
-
if (!str) return false;
|
|
171
|
-
// IPv4
|
|
172
|
-
if (/^(\d{1,3}\.){3}\d{1,3}$/.test(str)) {
|
|
173
|
-
return str.split(".").every((p) => {
|
|
174
|
-
const n = parseInt(p, 10);
|
|
175
|
-
return n >= 0 && n <= 255;
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
// IPv6 (basic check)
|
|
179
|
-
if (/^[0-9a-fA-F:]+$/.test(str) && str.includes(":")) return true;
|
|
180
|
-
return false;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function isTrustedProxyAddr(address) {
|
|
184
|
-
const trusted = process.env.FORGE_TRUSTED_PROXIES;
|
|
185
|
-
if (!trusted) return false;
|
|
186
|
-
if (!address) return false;
|
|
187
|
-
const noZone = address.split("%")[0];
|
|
188
|
-
const addr = noZone.toLowerCase().replace(/^(0{0,4}:){0,5}(0{0,4}:)?ffff:/, "").replace(/^::ffff:/, "");
|
|
189
|
-
for (const entry of trusted.split(",")) {
|
|
190
|
-
const trimmed = entry.trim();
|
|
191
|
-
if (addr === trimmed) return true;
|
|
192
|
-
}
|
|
193
|
-
return false;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function defaultKeyExtractor(req) {
|
|
197
|
-
const rawAddr = req.socket?.remoteAddress;
|
|
198
|
-
const remoteAddr = rawAddr ?? `unknown_${Math.random().toString(36).slice(2, 10)}`;
|
|
199
|
-
|
|
200
|
-
// H1: Only trust X-Forwarded-For from trusted proxies
|
|
201
|
-
// If FORGE_TRUSTED_PROXIES is set, only those addresses are trusted.
|
|
202
|
-
// Otherwise, fall back to trusting private networks for backwards compatibility.
|
|
203
|
-
const trusted = process.env.FORGE_TRUSTED_PROXIES;
|
|
204
|
-
const isProxyTrusted = trusted ? isTrustedProxyAddr(remoteAddr) : (rawAddr && isPrivateNetwork(remoteAddr));
|
|
205
|
-
if (isProxyTrusted) {
|
|
206
|
-
const xff = req.headers?.["x-forwarded-for"];
|
|
207
|
-
if (xff) {
|
|
208
|
-
const ips = xff.split(",").slice(0, 10).map((ip) => ip.trim());
|
|
209
|
-
// Walk right-to-left to find first untrusted IP (the real client)
|
|
210
|
-
for (let i = ips.length - 1; i >= 0; i--) {
|
|
211
|
-
if (!isValidIP(ips[i])) continue;
|
|
212
|
-
const ipTrusted = trusted ? isTrustedProxyAddr(ips[i]) : isPrivateNetwork(ips[i]);
|
|
213
|
-
if (ips[i] && !ipTrusted) {
|
|
214
|
-
return ips[i];
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Fallback to X-Real-IP (commonly set by nginx)
|
|
220
|
-
const realIp = req.headers?.["x-real-ip"];
|
|
221
|
-
if (realIp && isValidIP(realIp)) {
|
|
222
|
-
return realIp;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return remoteAddr;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Direct connection or no trusted proxies configured — use direct remote address
|
|
229
|
-
return remoteAddr;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// ─── Load Shedder ───────────────────────────────────────────
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Global load shedder. Monitors the gateway process health and
|
|
236
|
-
* drops requests when overloaded.
|
|
237
|
-
*
|
|
238
|
-
* Monitors two signals:
|
|
239
|
-
* 1. Event loop lag — if the event loop is delayed by > threshold,
|
|
240
|
-
* the process is CPU-starved. Start shedding.
|
|
241
|
-
* 2. Active requests — if we have too many in-flight, shed.
|
|
242
|
-
*
|
|
243
|
-
* Shedding is priority-based:
|
|
244
|
-
* - CRITICAL: health checks, auth — never shed
|
|
245
|
-
* - HIGH: payment webhooks — shed only at extreme load
|
|
246
|
-
* - NORMAL: regular API calls — shed when overloaded
|
|
247
|
-
* - LOW: analytics, logging — shed first
|
|
248
|
-
*/
|
|
249
|
-
export class LoadShedder {
|
|
250
|
-
/**
|
|
251
|
-
* @param {Object} options
|
|
252
|
-
* @param {number} [options.eventLoopThresholdMs=100] - Lag threshold to start shedding
|
|
253
|
-
* @param {number} [options.maxActiveRequests=5000] - Max in-flight before shedding
|
|
254
|
-
* @param {number} [options.checkIntervalMs=500] - How often to check event loop
|
|
255
|
-
* @param {Object} [options.priorities] - Path → priority overrides
|
|
256
|
-
*/
|
|
257
|
-
constructor(options = {}) {
|
|
258
|
-
this.eventLoopThresholdMs = options.eventLoopThresholdMs ?? 100;
|
|
259
|
-
this.maxActiveRequests = options.maxActiveRequests ?? 5000;
|
|
260
|
-
this.checkIntervalMs = options.checkIntervalMs ?? 500;
|
|
261
|
-
|
|
262
|
-
// Default priority rules
|
|
263
|
-
this.priorityRules = [
|
|
264
|
-
{ pattern: /^\/(health|ready|live)/, priority: "critical" },
|
|
265
|
-
{ pattern: /^\/auth(\/|$)|^\/login(\/|$)|^\/oauth(\/|$)/, priority: "critical" },
|
|
266
|
-
{ pattern: /^\/webhook(\/|$)|^\/stripe(\/|$)/, priority: "high" },
|
|
267
|
-
{ pattern: /^\/api\//, priority: "normal" },
|
|
268
|
-
{ pattern: /^\/analytics|^\/telemetry/, priority: "low" },
|
|
269
|
-
...(options.priorityRules ?? []),
|
|
270
|
-
];
|
|
271
|
-
|
|
272
|
-
this.activeRequests = 0;
|
|
273
|
-
this.eventLoopLag = 0;
|
|
274
|
-
this._lastCheck = Date.now();
|
|
275
|
-
|
|
276
|
-
// Shedding thresholds by priority
|
|
277
|
-
this.shedThresholds = {
|
|
278
|
-
low: { lagMs: 50, loadRatio: 0.6 },
|
|
279
|
-
normal: { lagMs: 100, loadRatio: 0.8 },
|
|
280
|
-
high: { lagMs: 200, loadRatio: 0.95 },
|
|
281
|
-
critical: { lagMs: Infinity, loadRatio: Infinity }, // never shed
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
// Start event loop lag measurement using recursive setTimeout to prevent drift
|
|
285
|
-
this._lagTimerActive = true;
|
|
286
|
-
const measureLag = () => {
|
|
287
|
-
if (!this._lagTimerActive) return;
|
|
288
|
-
const now = Date.now();
|
|
289
|
-
const expected = this.checkIntervalMs;
|
|
290
|
-
const actual = now - this._lastCheck;
|
|
291
|
-
this.eventLoopLag = Math.max(0, actual - expected);
|
|
292
|
-
this._lastCheck = now;
|
|
293
|
-
this._lagTimer = setTimeout(measureLag, this.checkIntervalMs);
|
|
294
|
-
this._lagTimer.unref();
|
|
295
|
-
};
|
|
296
|
-
this._lagTimer = setTimeout(measureLag, this.checkIntervalMs);
|
|
297
|
-
this._lagTimer.unref();
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/**
|
|
301
|
-
* Should this request be accepted or shed?
|
|
302
|
-
* @param {Object} req - HTTP request
|
|
303
|
-
* @returns {{ accept: boolean, reason?: string, priority: string }}
|
|
304
|
-
*/
|
|
305
|
-
shouldAccept(req) {
|
|
306
|
-
const priority = this._getPriority(req);
|
|
307
|
-
const threshold = this.shedThresholds[priority];
|
|
308
|
-
const loadRatio = this.activeRequests / this.maxActiveRequests;
|
|
309
|
-
|
|
310
|
-
// Check event loop lag
|
|
311
|
-
if (this.eventLoopLag > threshold.lagMs) {
|
|
312
|
-
return {
|
|
313
|
-
accept: false,
|
|
314
|
-
reason: `Event loop lag ${this.eventLoopLag}ms exceeds ${threshold.lagMs}ms threshold for ${priority} priority`,
|
|
315
|
-
priority,
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// Check active request count
|
|
320
|
-
if (loadRatio > threshold.loadRatio) {
|
|
321
|
-
return {
|
|
322
|
-
accept: false,
|
|
323
|
-
reason: `Load at ${(loadRatio * 100).toFixed(0)}% exceeds ${(threshold.loadRatio * 100).toFixed(0)}% threshold for ${priority} priority`,
|
|
324
|
-
priority,
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
return { accept: true, priority };
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
_getPriority(req) {
|
|
332
|
-
const url = req.url ?? "/";
|
|
333
|
-
for (const rule of this.priorityRules) {
|
|
334
|
-
if (rule.pattern.test(url)) {
|
|
335
|
-
return rule.priority;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
return "normal";
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
trackRequest() {
|
|
342
|
-
this.activeRequests++;
|
|
343
|
-
return () => {
|
|
344
|
-
this.activeRequests--;
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
get stats() {
|
|
349
|
-
return {
|
|
350
|
-
activeRequests: this.activeRequests,
|
|
351
|
-
maxActiveRequests: this.maxActiveRequests,
|
|
352
|
-
eventLoopLagMs: this.eventLoopLag,
|
|
353
|
-
loadPercent: ((this.activeRequests / this.maxActiveRequests) * 100).toFixed(1),
|
|
354
|
-
shedding:
|
|
355
|
-
this.eventLoopLag > this.shedThresholds.normal.lagMs || this.activeRequests > this.maxActiveRequests * 0.8,
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
stop() {
|
|
360
|
-
this._lagTimerActive = false;
|
|
361
|
-
if (this._lagTimer) clearTimeout(this._lagTimer);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// ─── Adaptive Concurrency Limiter ───────────────────────────
|
|
366
|
-
|
|
367
|
-
/**
|
|
368
|
-
* Adaptive Concurrency Limiter (per downstream service)
|
|
369
|
-
*
|
|
370
|
-
* The core problem: how many concurrent requests should we send
|
|
371
|
-
* to the users service? Too few = wasted capacity. Too many =
|
|
372
|
-
* overwhelm the service, latency spikes, cascade failure.
|
|
373
|
-
*
|
|
374
|
-
* Fixed limits are fragile — they're either too conservative
|
|
375
|
-
* (wasting capacity during normal load) or too aggressive
|
|
376
|
-
* (causing failures during peak).
|
|
377
|
-
*
|
|
378
|
-
* Instead, we use the Vegas algorithm (from TCP congestion control):
|
|
379
|
-
*
|
|
380
|
-
* 1. Start with a small concurrency window (e.g., 10)
|
|
381
|
-
* 2. Measure round-trip latency for each request
|
|
382
|
-
* 3. Track the minimum observed latency (the "no-load" baseline)
|
|
383
|
-
* 4. If current latency ≈ baseline → increase the window
|
|
384
|
-
* (service has capacity, give it more)
|
|
385
|
-
* 5. If current latency >> baseline → decrease the window
|
|
386
|
-
* (service is queuing, back off)
|
|
387
|
-
*
|
|
388
|
-
* This automatically adapts to:
|
|
389
|
-
* - Fast services (high window, more concurrent requests)
|
|
390
|
-
* - Slow services (low window, fewer concurrent requests)
|
|
391
|
-
* - Services that degrade under load (window shrinks as latency rises)
|
|
392
|
-
* - Services that recover (window grows as latency drops)
|
|
393
|
-
*
|
|
394
|
-
* Inspired by Netflix's concurrency-limits library.
|
|
395
|
-
*/
|
|
396
|
-
export class AdaptiveConcurrencyLimiter {
|
|
397
|
-
/**
|
|
398
|
-
* @param {string} serviceName
|
|
399
|
-
* @param {Object} options
|
|
400
|
-
* @param {number} [options.initialLimit=20] - Starting concurrency limit
|
|
401
|
-
* @param {number} [options.minLimit=5] - Floor
|
|
402
|
-
* @param {number} [options.maxLimit=500] - Ceiling
|
|
403
|
-
* @param {number} [options.smoothing=0.2] - EMA smoothing for latency
|
|
404
|
-
* @param {number} [options.tolerance=2.0] - Ratio of current/min latency
|
|
405
|
-
* before we reduce the limit
|
|
406
|
-
*/
|
|
407
|
-
constructor(serviceName, options = {}) {
|
|
408
|
-
this.serviceName = serviceName;
|
|
409
|
-
this.limit = options.initialLimit ?? 20;
|
|
410
|
-
this.minLimit = options.minLimit ?? 5;
|
|
411
|
-
this.maxLimit = options.maxLimit ?? 500;
|
|
412
|
-
this.smoothing = options.smoothing ?? 0.2;
|
|
413
|
-
this.tolerance = options.tolerance ?? 2.0;
|
|
414
|
-
|
|
415
|
-
this.inFlight = 0;
|
|
416
|
-
this.minLatency = Infinity;
|
|
417
|
-
this.smoothedLatency = 0;
|
|
418
|
-
|
|
419
|
-
// Metrics
|
|
420
|
-
this.totalRequests = 0;
|
|
421
|
-
this.totalRejected = 0;
|
|
422
|
-
this.totalSuccesses = 0;
|
|
423
|
-
this.totalFailures = 0;
|
|
424
|
-
|
|
425
|
-
// Latency percentiles (ring buffer)
|
|
426
|
-
this._latencies = new Float64Array(1000);
|
|
427
|
-
this._latencyIdx = 0;
|
|
428
|
-
this._latencyCount = 0;
|
|
429
|
-
|
|
430
|
-
// Windowed minimum latency reset
|
|
431
|
-
this._minLatencyWindow = new Float64Array(1000);
|
|
432
|
-
this._minLatencyWindowIdx = 0;
|
|
433
|
-
this._minLatencyWindowCount = 0;
|
|
434
|
-
this._minLatencyObservations = 0;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* Try to acquire a concurrency slot.
|
|
439
|
-
* @returns {{ acquired: boolean, release?: Function }}
|
|
440
|
-
*/
|
|
441
|
-
tryAcquire() {
|
|
442
|
-
this.totalRequests++;
|
|
443
|
-
|
|
444
|
-
if (this.inFlight >= this.limit) {
|
|
445
|
-
this.totalRejected++;
|
|
446
|
-
return { acquired: false };
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
this.inFlight++;
|
|
450
|
-
const startTime = performance.now();
|
|
451
|
-
|
|
452
|
-
const release = (success = true) => {
|
|
453
|
-
this.inFlight--;
|
|
454
|
-
const latency = performance.now() - startTime;
|
|
455
|
-
this._recordLatency(latency, success);
|
|
456
|
-
};
|
|
457
|
-
|
|
458
|
-
return { acquired: true, release };
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
_recordLatency(latencyMs, success) {
|
|
462
|
-
if (success) {
|
|
463
|
-
this.totalSuccesses++;
|
|
464
|
-
|
|
465
|
-
// Track minimum latency with windowed reset
|
|
466
|
-
this._minLatencyWindow[this._minLatencyWindowIdx % this._minLatencyWindow.length] = latencyMs;
|
|
467
|
-
this._minLatencyWindowIdx++;
|
|
468
|
-
this._minLatencyWindowCount = Math.min(this._minLatencyWindowCount + 1, this._minLatencyWindow.length);
|
|
469
|
-
this._minLatencyObservations++;
|
|
470
|
-
|
|
471
|
-
if (latencyMs < this.minLatency) {
|
|
472
|
-
this.minLatency = latencyMs;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// Every 1000 observations, recalculate minLatency from the window
|
|
476
|
-
if (this._minLatencyObservations >= 1000) {
|
|
477
|
-
this._minLatencyObservations = 0;
|
|
478
|
-
let windowMin = Infinity;
|
|
479
|
-
for (let i = 0; i < this._minLatencyWindowCount; i++) {
|
|
480
|
-
if (this._minLatencyWindow[i] < windowMin) {
|
|
481
|
-
windowMin = this._minLatencyWindow[i];
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
this.minLatency = windowMin;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// Exponential moving average
|
|
488
|
-
if (this.smoothedLatency === 0) {
|
|
489
|
-
this.smoothedLatency = latencyMs;
|
|
490
|
-
} else {
|
|
491
|
-
this.smoothedLatency = this.smoothing * latencyMs + (1 - this.smoothing) * this.smoothedLatency;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// Store in ring buffer for percentile calculation
|
|
495
|
-
this._latencies[this._latencyIdx % this._latencies.length] = latencyMs;
|
|
496
|
-
this._latencyIdx++;
|
|
497
|
-
this._latencyCount = Math.min(this._latencyCount + 1, this._latencies.length);
|
|
498
|
-
|
|
499
|
-
// Adjust limit using Vegas algorithm
|
|
500
|
-
this._adjustLimit();
|
|
501
|
-
} else {
|
|
502
|
-
this.totalFailures++;
|
|
503
|
-
// On failure, aggressively reduce limit
|
|
504
|
-
this.limit = Math.max(this.minLimit, Math.floor(this.limit * 0.75));
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
_adjustLimit() {
|
|
509
|
-
if (this.minLatency === Infinity || this.smoothedLatency === 0) return;
|
|
510
|
-
|
|
511
|
-
const ratio = this.smoothedLatency / this.minLatency;
|
|
512
|
-
|
|
513
|
-
if (ratio < this.tolerance) {
|
|
514
|
-
// Latency is close to baseline — we have room, increase slowly
|
|
515
|
-
// The gradient determines how aggressively we grow
|
|
516
|
-
const gradient = Math.max(0, (this.tolerance - ratio) / this.tolerance);
|
|
517
|
-
const increase = Math.ceil(gradient * 2);
|
|
518
|
-
this.limit = Math.min(this.maxLimit, this.limit + increase);
|
|
519
|
-
} else {
|
|
520
|
-
// Latency is elevated — service is queuing, reduce
|
|
521
|
-
// Reduce proportionally to how far over the tolerance we are
|
|
522
|
-
const overshoot = ratio / this.tolerance;
|
|
523
|
-
const decrease = Math.ceil(overshoot);
|
|
524
|
-
this.limit = Math.max(this.minLimit, this.limit - decrease);
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
get stats() {
|
|
529
|
-
return {
|
|
530
|
-
service: this.serviceName,
|
|
531
|
-
currentLimit: this.limit,
|
|
532
|
-
inFlight: this.inFlight,
|
|
533
|
-
utilization: `${((this.inFlight / this.limit) * 100).toFixed(0)}%`,
|
|
534
|
-
minLatencyMs: this.minLatency === Infinity ? null : this.minLatency.toFixed(2),
|
|
535
|
-
smoothedLatencyMs: this.smoothedLatency.toFixed(2),
|
|
536
|
-
p99LatencyMs: this._getPercentile(0.99)?.toFixed(2) ?? null,
|
|
537
|
-
totalRequests: this.totalRequests,
|
|
538
|
-
totalRejected: this.totalRejected,
|
|
539
|
-
rejectionRate: `${((this.totalRejected / Math.max(1, this.totalRequests)) * 100).toFixed(1)}%`,
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
_getPercentile(p) {
|
|
544
|
-
if (this._latencyCount === 0) return null;
|
|
545
|
-
const sorted = Array.from(this._latencies.slice(0, this._latencyCount)).sort((a, b) => a - b);
|
|
546
|
-
const idx = Math.floor(sorted.length * p);
|
|
547
|
-
return sorted[idx];
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// ─── IngressProtection (combines all layers) ────────────────
|
|
552
|
-
|
|
553
|
-
/**
|
|
554
|
-
* Combined ingress protection middleware.
|
|
555
|
-
*
|
|
556
|
-
* Applies all layers in order: connection limit → rate limit →
|
|
557
|
-
* load shedding → route → adaptive concurrency → service.
|
|
558
|
-
*/
|
|
559
|
-
export class IngressProtection extends EventEmitter {
|
|
560
|
-
/**
|
|
561
|
-
* @param {Object} options
|
|
562
|
-
* @param {number} [options.maxConnections=10000]
|
|
563
|
-
* @param {Object} [options.rateLimit]
|
|
564
|
-
* @param {Object} [options.loadShedding]
|
|
565
|
-
* @param {Object} [options.services] - Per-service concurrency config
|
|
566
|
-
*/
|
|
567
|
-
constructor(options = {}) {
|
|
568
|
-
super();
|
|
569
|
-
|
|
570
|
-
this.maxConnections = options.maxConnections ?? 10000;
|
|
571
|
-
this.activeConnections = 0;
|
|
572
|
-
|
|
573
|
-
this.rateLimiter = new RateLimiter(options.rateLimit ?? {});
|
|
574
|
-
this.loadShedder = new LoadShedder(options.loadShedding ?? {});
|
|
575
|
-
|
|
576
|
-
// Per-service adaptive concurrency limiters
|
|
577
|
-
/** @type {Map<string, AdaptiveConcurrencyLimiter>} */
|
|
578
|
-
this.serviceLimiters = new Map();
|
|
579
|
-
|
|
580
|
-
if (options.services) {
|
|
581
|
-
for (const [name, config] of Object.entries(options.services)) {
|
|
582
|
-
this.serviceLimiters.set(name, new AdaptiveConcurrencyLimiter(name, config));
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
this._metricsInterval = setInterval(() => {
|
|
587
|
-
this.emit("metrics", this.stats);
|
|
588
|
-
}, 10000);
|
|
589
|
-
this._metricsInterval.unref();
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
/**
|
|
593
|
-
* Get or create a concurrency limiter for a service.
|
|
594
|
-
*/
|
|
595
|
-
getServiceLimiter(serviceName) {
|
|
596
|
-
if (!this.serviceLimiters.has(serviceName)) {
|
|
597
|
-
this.serviceLimiters.set(serviceName, new AdaptiveConcurrencyLimiter(serviceName));
|
|
598
|
-
}
|
|
599
|
-
return this.serviceLimiters.get(serviceName);
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
/**
|
|
603
|
-
* Returns a middleware function for the ForgeContext router.
|
|
604
|
-
*
|
|
605
|
-
* Usage:
|
|
606
|
-
* ctx.router.use(this.ingress.middleware());
|
|
607
|
-
*/
|
|
608
|
-
middleware() {
|
|
609
|
-
return async (req, res, next) => {
|
|
610
|
-
// Layer 1: Connection limit
|
|
611
|
-
if (this.activeConnections >= this.maxConnections) {
|
|
612
|
-
res.writeHead(503, { "Retry-After": "5" });
|
|
613
|
-
res.end(JSON.stringify({ error: "Server at connection capacity" }));
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
this.activeConnections++;
|
|
617
|
-
let connectionCounted = true;
|
|
618
|
-
req.on("close", () => {
|
|
619
|
-
if (connectionCounted) {
|
|
620
|
-
this.activeConnections--;
|
|
621
|
-
connectionCounted = false;
|
|
622
|
-
}
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
// Layer 2: Rate limiting
|
|
626
|
-
const rateCheck = this.rateLimiter.check(req);
|
|
627
|
-
if (!rateCheck.allowed) {
|
|
628
|
-
const retryAfterSec = Math.ceil(rateCheck.retryAfter / 1000);
|
|
629
|
-
res.writeHead(429, {
|
|
630
|
-
"Retry-After": String(retryAfterSec),
|
|
631
|
-
"X-RateLimit-Remaining": "0",
|
|
632
|
-
"X-RateLimit-Reset": String(retryAfterSec),
|
|
633
|
-
});
|
|
634
|
-
res.end(
|
|
635
|
-
JSON.stringify({
|
|
636
|
-
error: "Rate limit exceeded",
|
|
637
|
-
retryAfter: retryAfterSec,
|
|
638
|
-
}),
|
|
639
|
-
);
|
|
640
|
-
if (connectionCounted) {
|
|
641
|
-
connectionCounted = false;
|
|
642
|
-
this.activeConnections--;
|
|
643
|
-
}
|
|
644
|
-
return;
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
// Layer 3: Load shedding
|
|
648
|
-
const shedCheck = this.loadShedder.shouldAccept(req);
|
|
649
|
-
if (!shedCheck.accept) {
|
|
650
|
-
res.writeHead(503, { "Retry-After": "1" });
|
|
651
|
-
// Don't expose internal load/lag details to clients
|
|
652
|
-
res.end(
|
|
653
|
-
JSON.stringify({
|
|
654
|
-
error: "Service temporarily overloaded",
|
|
655
|
-
}),
|
|
656
|
-
);
|
|
657
|
-
if (connectionCounted) {
|
|
658
|
-
connectionCounted = false;
|
|
659
|
-
this.activeConnections--;
|
|
660
|
-
}
|
|
661
|
-
return;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// Track active request
|
|
665
|
-
const releaseRequest = this.loadShedder.trackRequest();
|
|
666
|
-
|
|
667
|
-
// Add headers
|
|
668
|
-
res.setHeader("X-RateLimit-Remaining", String(rateCheck.remaining));
|
|
669
|
-
|
|
670
|
-
// Track completion by wrapping res.end — also hook close for premature disconnects
|
|
671
|
-
let requestReleased = false;
|
|
672
|
-
const doRelease = () => {
|
|
673
|
-
if (!requestReleased) {
|
|
674
|
-
requestReleased = true;
|
|
675
|
-
releaseRequest();
|
|
676
|
-
// Clean up close listener to avoid accumulation
|
|
677
|
-
if (typeof res.removeListener === "function") {
|
|
678
|
-
res.removeListener("close", doRelease);
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
};
|
|
682
|
-
|
|
683
|
-
const origEnd = res.end.bind(res);
|
|
684
|
-
res.end = (...args) => {
|
|
685
|
-
doRelease();
|
|
686
|
-
return origEnd(...args);
|
|
687
|
-
};
|
|
688
|
-
|
|
689
|
-
// If client disconnects before res.end(), still release
|
|
690
|
-
if (typeof res.on === "function") res.on("close", doRelease);
|
|
691
|
-
|
|
692
|
-
// Pass through to router
|
|
693
|
-
if (next) next();
|
|
694
|
-
};
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
/**
|
|
698
|
-
* Wrap a proxy call with adaptive concurrency limiting.
|
|
699
|
-
*
|
|
700
|
-
* Usage in the gateway:
|
|
701
|
-
* const user = await this.ingress.withConcurrencyLimit('users', () =>
|
|
702
|
-
* this.users.getUser(id)
|
|
703
|
-
* );
|
|
704
|
-
*
|
|
705
|
-
* Or in the proxy interceptor layer:
|
|
706
|
-
* The ServiceProxy automatically calls this if ingress is configured.
|
|
707
|
-
*/
|
|
708
|
-
async withConcurrencyLimit(serviceName, fn) {
|
|
709
|
-
const limiter = this.getServiceLimiter(serviceName);
|
|
710
|
-
const slot = limiter.tryAcquire();
|
|
711
|
-
|
|
712
|
-
if (!slot.acquired) {
|
|
713
|
-
const err = new Error(
|
|
714
|
-
`Service "${serviceName}" at concurrency limit (${limiter.limit}). ` +
|
|
715
|
-
`${limiter.inFlight} requests in flight.`,
|
|
716
|
-
);
|
|
717
|
-
err.code = "CONCURRENCY_LIMIT";
|
|
718
|
-
err.statusCode = 503;
|
|
719
|
-
throw err;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
try {
|
|
723
|
-
const result = await fn();
|
|
724
|
-
slot.release(true);
|
|
725
|
-
return result;
|
|
726
|
-
} catch (err) {
|
|
727
|
-
slot.release(false);
|
|
728
|
-
throw err;
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
get stats() {
|
|
733
|
-
const serviceStats = {};
|
|
734
|
-
for (const [name, limiter] of this.serviceLimiters) {
|
|
735
|
-
serviceStats[name] = limiter.stats;
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
return {
|
|
739
|
-
connections: {
|
|
740
|
-
active: this.activeConnections,
|
|
741
|
-
max: this.maxConnections,
|
|
742
|
-
},
|
|
743
|
-
rateLimiter: this.rateLimiter.stats,
|
|
744
|
-
loadShedder: this.loadShedder.stats,
|
|
745
|
-
services: serviceStats,
|
|
746
|
-
};
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
/**
|
|
750
|
-
* HTTP handler for ingress stats endpoint.
|
|
751
|
-
* Mount on the metrics server:
|
|
752
|
-
* GET /ingress → full stats
|
|
753
|
-
*/
|
|
754
|
-
httpHandler(req, res) {
|
|
755
|
-
if (req.url === "/ingress") {
|
|
756
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
757
|
-
res.end(JSON.stringify(this.stats, null, 2));
|
|
758
|
-
return true;
|
|
759
|
-
}
|
|
760
|
-
return false;
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
stop() {
|
|
764
|
-
this.rateLimiter.stop();
|
|
765
|
-
this.loadShedder.stop();
|
|
766
|
-
if (this._metricsInterval) clearInterval(this._metricsInterval);
|
|
767
|
-
}
|
|
768
|
-
}
|