Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 437d1a3329 | |||
| 746d93663d | |||
| a3f3fee253 | |||
| 53dee1fffc | |||
| 34dc0cb9b6 | |||
| c83c43194b | |||
| d026d7c266 | |||
| 3b01144c51 | |||
| 56f5697e1b | |||
| f04875885f |
33
changelog.md
33
changelog.md
@@ -1,5 +1,38 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-03-25 - 26.2.3 - fix(repo)
|
||||||
|
no changes to commit
|
||||||
|
|
||||||
|
|
||||||
|
## 2026-03-25 - 26.2.2 - fix(proxy)
|
||||||
|
improve connection cleanup and route validation handling
|
||||||
|
|
||||||
|
- add timeouts for HTTP/1 upstream connection drivers to prevent lingering tasks
|
||||||
|
- ensure QUIC relay sessions cancel and abort background tasks on drop
|
||||||
|
- avoid registering unnamed routes as duplicates and label unnamed catch-all conflicts clearly
|
||||||
|
- fix offset mapping route helper to forward only remaining route options without overriding derived values
|
||||||
|
- update project config filename and toolchain versions for the current build setup
|
||||||
|
|
||||||
|
## 2026-03-23 - 26.2.1 - fix(rustproxy-http)
|
||||||
|
include the upstream request URL when caching H3 Alt-Svc discoveries
|
||||||
|
|
||||||
|
- Tracks the request path that triggered Alt-Svc discovery in connection activity state
|
||||||
|
- Adds request URL context to Alt-Svc debug logging and protocol cache insertion reasons for better traceability
|
||||||
|
|
||||||
|
## 2026-03-23 - 26.2.0 - feat(protocol-cache)
|
||||||
|
add sliding TTL re-probing and eviction for backend protocol detection
|
||||||
|
|
||||||
|
- extend protocol cache entries and metrics with last accessed and last probed timestamps
|
||||||
|
- trigger periodic ALPN re-probes for cached H1/H2 entries while keeping active entries alive with a sliding 1 day TTL
|
||||||
|
- log protocol transitions with reasons and evict cache entries when all protocol fallback attempts fail
|
||||||
|
|
||||||
|
## 2026-03-22 - 26.1.0 - feat(rustproxy-http)
|
||||||
|
add protocol failure suppression, h3 fallback escalation, and protocol cache metrics exposure
|
||||||
|
|
||||||
|
- introduces escalating cooldowns for failed H2/H3 protocol detection to prevent repeated upgrades to unstable backends
|
||||||
|
- adds within-request escalation to cached HTTP/3 when TCP or TLS backend connections fail in auto-detect mode
|
||||||
|
- exposes detected protocol cache entries and suppression state through Rust metrics and the TypeScript metrics adapter
|
||||||
|
|
||||||
## 2026-03-21 - 26.0.0 - BREAKING CHANGE(ts-api,rustproxy)
|
## 2026-03-21 - 26.0.0 - BREAKING CHANGE(ts-api,rustproxy)
|
||||||
remove deprecated TypeScript protocol and utility exports while hardening QUIC, HTTP/3, WebSocket, and rate limiter cleanup paths
|
remove deprecated TypeScript protocol and utility exports while hardening QUIC, HTTP/3, WebSocket, and rate limiter cleanup paths
|
||||||
|
|
||||||
|
|||||||
2
license
2
license
@@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2019 Lossless GmbH (hello@lossless.com)
|
Copyright (c) 2019 Task Venture Capital GmbH (hello@task.vc)
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
16
package.json
16
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartproxy",
|
"name": "@push.rocks/smartproxy",
|
||||||
"version": "26.0.0",
|
"version": "26.2.3",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.",
|
"description": "A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
@@ -16,13 +16,13 @@
|
|||||||
"buildDocs": "tsdoc"
|
"buildDocs": "tsdoc"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^4.3.0",
|
"@git.zone/tsbuild": "^4.4.0",
|
||||||
"@git.zone/tsrun": "^2.0.1",
|
"@git.zone/tsrun": "^2.0.2",
|
||||||
"@git.zone/tsrust": "^1.3.0",
|
"@git.zone/tsrust": "^1.3.2",
|
||||||
"@git.zone/tstest": "^3.5.0",
|
"@git.zone/tstest": "^3.6.0",
|
||||||
"@push.rocks/smartserve": "^2.0.1",
|
"@push.rocks/smartserve": "^2.0.3",
|
||||||
"@types/node": "^25.5.0",
|
"@types/node": "^25.5.0",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^6.0.2",
|
||||||
"why-is-node-running": "^3.2.2"
|
"why-is-node-running": "^3.2.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
"dist_ts_web/**/*",
|
"dist_ts_web/**/*",
|
||||||
"assets/**/*",
|
"assets/**/*",
|
||||||
"cli.js",
|
"cli.js",
|
||||||
"npmextra.json",
|
".smartconfig.json",
|
||||||
"readme.md",
|
"readme.md",
|
||||||
"changelog.md"
|
"changelog.md"
|
||||||
],
|
],
|
||||||
|
|||||||
469
pnpm-lock.yaml
generated
469
pnpm-lock.yaml
generated
@@ -25,26 +25,26 @@ importers:
|
|||||||
version: 10.2.4
|
version: 10.2.4
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@git.zone/tsbuild':
|
'@git.zone/tsbuild':
|
||||||
specifier: ^4.3.0
|
specifier: ^4.4.0
|
||||||
version: 4.3.0
|
version: 4.4.0
|
||||||
'@git.zone/tsrun':
|
'@git.zone/tsrun':
|
||||||
specifier: ^2.0.1
|
specifier: ^2.0.2
|
||||||
version: 2.0.1
|
version: 2.0.2
|
||||||
'@git.zone/tsrust':
|
'@git.zone/tsrust':
|
||||||
specifier: ^1.3.0
|
specifier: ^1.3.2
|
||||||
version: 1.3.0
|
version: 1.3.2
|
||||||
'@git.zone/tstest':
|
'@git.zone/tstest':
|
||||||
specifier: ^3.5.0
|
specifier: ^3.6.0
|
||||||
version: 3.5.0(socks@2.8.7)(typescript@5.9.3)
|
version: 3.6.0(socks@2.8.7)(typescript@6.0.2)
|
||||||
'@push.rocks/smartserve':
|
'@push.rocks/smartserve':
|
||||||
specifier: ^2.0.1
|
specifier: ^2.0.3
|
||||||
version: 2.0.1
|
version: 2.0.3
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^25.5.0
|
specifier: ^25.5.0
|
||||||
version: 25.5.0
|
version: 25.5.0
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.9.3
|
specifier: ^6.0.2
|
||||||
version: 5.9.3
|
version: 6.0.2
|
||||||
why-is-node-running:
|
why-is-node-running:
|
||||||
specifier: ^3.2.2
|
specifier: ^3.2.2
|
||||||
version: 3.2.2
|
version: 3.2.2
|
||||||
@@ -414,28 +414,28 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@git.zone/tsbuild@4.3.0':
|
'@git.zone/tsbuild@4.4.0':
|
||||||
resolution: {integrity: sha512-lb6eMQ8RQPaJqAB4kC++GIElOiTAH1pClmoND/q7XHuiMZxv6cXz2/U/sZt339mon2c40dXRG2tkLF2jRsP0pQ==}
|
resolution: {integrity: sha512-98igHfppi6blFYDyzNukNkj4FUO5ZlyXEaSyJh8vCkkZM8SyAgfZj+NUWA1D1iaPXE58UvK1Pt/o8p8iI9UHHw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@git.zone/tsbundle@2.9.1':
|
'@git.zone/tsbundle@2.10.0':
|
||||||
resolution: {integrity: sha512-JW1xjSv7UjAm2lwAQPxhCWs14wqs+UIq5FqIGUPuI6rrDBWIMT2d0gpP6iP6TqXqgm6XpBlfU4rHcHheUXzXbQ==}
|
resolution: {integrity: sha512-dw2VFlgKssDlCxg92wSPiiAKwfCjJBOEOYXq1xO91OpjQLOkyogCxSLy0jzQ2BYnt4qmBnapjamzYzVjCr4CWg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@git.zone/tspublish@1.11.2':
|
'@git.zone/tspublish@1.11.5':
|
||||||
resolution: {integrity: sha512-BcGap1OzXDgXpfQXMh9W17r/CkWNhPsJ3WzjG2wrGE+ePUJCJAm9w6+J8G5WdZZcZKPqTB07cp707LbSiksc5A==}
|
resolution: {integrity: sha512-3tCGhVbH6S/17n3A6Tc6H+ncRdxxbTT0ABcj8S1wRLA8YuBSj9bY7k6uj/iFRy/B/OepB94m1goCiaWESdcZYg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@git.zone/tsrun@2.0.1':
|
'@git.zone/tsrun@2.0.2':
|
||||||
resolution: {integrity: sha512-NEcnsjvlC1o3Z6SS3VhKCf6Ev+Sh4EAinmggslrIR/ppMrvjDbXNFXoyr3PB+GLeSAR0JRZ1fGvVYjpEzjBdIg==}
|
resolution: {integrity: sha512-Rnp/wYHzI8A1pVBKOOePRJgQiBZdW+GEjpQk2uhvXz6A+ljUV2SXKc7NpQVVDsjEZaNFeAI9jMYOdk3lm3yMDA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@git.zone/tsrust@1.3.0':
|
'@git.zone/tsrust@1.3.2':
|
||||||
resolution: {integrity: sha512-dvmTAiM04Pkd7J1Gail3fu7aasmILQhC5vKL71/g6HYhpvl16/c+Dj3We5G4HsFr0jvAr+Xu570ZGEuZrtRcCg==}
|
resolution: {integrity: sha512-bUGomPk++He47Q6rnd9bihX6qoYtXgp9BtroBnNADk3q8WGyHivAcPwqIe4Bk32eByzW1Acc37u/h5gb/V8ekA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@git.zone/tstest@3.5.0':
|
'@git.zone/tstest@3.6.0':
|
||||||
resolution: {integrity: sha512-ugIJzdVkbgqSSw08SZajE7TB01GIYjEAmIy67O5skhvOyszGifwzJdR+8dS1VbQGlUUWQZMGQ2IowllHbAZYJQ==}
|
resolution: {integrity: sha512-5D6COywCXmCqeUB8v6/kOzjEWCTKTUTI3ZB99ebwEibENFXnFBoVxNSRKN0pSmBYlgBEkT7DLNfTfp5tclSg8A==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@img/colour@1.1.0':
|
'@img/colour@1.1.0':
|
||||||
@@ -783,8 +783,8 @@ packages:
|
|||||||
'@napi-rs/wasm-runtime@1.1.1':
|
'@napi-rs/wasm-runtime@1.1.1':
|
||||||
resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
|
resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
|
||||||
|
|
||||||
'@oxc-project/types@0.99.0':
|
'@oxc-project/types@0.122.0':
|
||||||
resolution: {integrity: sha512-LLDEhXB7g1m5J+woRSgfKsFPS3LhR9xRhTeIoEBm5WrkwMxn6eZ0Ld0c0K5eHB57ChZX6I3uSmmLjZ8pcjlRcw==}
|
resolution: {integrity: sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==}
|
||||||
|
|
||||||
'@pdf-lib/standard-fonts@1.0.0':
|
'@pdf-lib/standard-fonts@1.0.0':
|
||||||
resolution: {integrity: sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==}
|
resolution: {integrity: sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==}
|
||||||
@@ -858,12 +858,12 @@ packages:
|
|||||||
'@push.rocks/lik@6.3.1':
|
'@push.rocks/lik@6.3.1':
|
||||||
resolution: {integrity: sha512-UWDwGBaVx5yPtAFXqDDBtQZCzETUOA/7myQIXb+YBsuiIw4yQuhNZ23uY2ChQH2Zn6DLqdNSgQcYC0WywMZBNQ==}
|
resolution: {integrity: sha512-UWDwGBaVx5yPtAFXqDDBtQZCzETUOA/7myQIXb+YBsuiIw4yQuhNZ23uY2ChQH2Zn6DLqdNSgQcYC0WywMZBNQ==}
|
||||||
|
|
||||||
|
'@push.rocks/lik@6.4.0':
|
||||||
|
resolution: {integrity: sha512-GCdXyF2a6NP+i0W6Mib1PjtA6JGrl6Ae17SbaQwqTscn4JHNta6xm9r+D8/b83XGZsoU903FlJZli3YqJCxT9Q==}
|
||||||
|
|
||||||
'@push.rocks/mongodump@1.1.0':
|
'@push.rocks/mongodump@1.1.0':
|
||||||
resolution: {integrity: sha512-kW0ZUGyf1e4nwloVwBQjNId+MzgTcNS834C+RxH21i1NqyOubbpWZtJtPP+K+s35nSJRyCTy3ICfBMdDBTAm2w==}
|
resolution: {integrity: sha512-kW0ZUGyf1e4nwloVwBQjNId+MzgTcNS834C+RxH21i1NqyOubbpWZtJtPP+K+s35nSJRyCTy3ICfBMdDBTAm2w==}
|
||||||
|
|
||||||
'@push.rocks/npmextra@5.3.3':
|
|
||||||
resolution: {integrity: sha512-snLpSHwaQ5OXlZzF1KX/FY71W5LwajjBzor82Vue0smjEPnSeUPY5/JcVdMwtdprdJe13pc/EQQuIiL/zw4/yg==}
|
|
||||||
|
|
||||||
'@push.rocks/qenv@6.1.3':
|
'@push.rocks/qenv@6.1.3':
|
||||||
resolution: {integrity: sha512-+z2hsAU/7CIgpYLFqvda8cn9rUBMHqLdQLjsFfRn5jPoD7dJ5rFlpkbhfM4Ws8mHMniwWaxGKo+q/YBhtzRBLg==}
|
resolution: {integrity: sha512-+z2hsAU/7CIgpYLFqvda8cn9rUBMHqLdQLjsFfRn5jPoD7dJ5rFlpkbhfM4Ws8mHMniwWaxGKo+q/YBhtzRBLg==}
|
||||||
|
|
||||||
@@ -888,6 +888,9 @@ packages:
|
|||||||
'@push.rocks/smartclickhouse@2.2.0':
|
'@push.rocks/smartclickhouse@2.2.0':
|
||||||
resolution: {integrity: sha512-eTzKiREIPSzL1kPkVyD6vEbn+WV/DvQqDjP67VlhNlQGbRcemnJG/eLrUUR1ytmdIqnsZGEK6UYBgyj5nhzLNQ==}
|
resolution: {integrity: sha512-eTzKiREIPSzL1kPkVyD6vEbn+WV/DvQqDjP67VlhNlQGbRcemnJG/eLrUUR1ytmdIqnsZGEK6UYBgyj5nhzLNQ==}
|
||||||
|
|
||||||
|
'@push.rocks/smartconfig@6.1.0':
|
||||||
|
resolution: {integrity: sha512-B+xh63PhGAsSwuRyCKXr4PAjJ4HoVKhNysi67OGY6gGqGm6uopgEW1cvrUZ7T5ZSck9KlVx7ZTugbqm6dqBK1Q==}
|
||||||
|
|
||||||
'@push.rocks/smartcrypto@2.0.4':
|
'@push.rocks/smartcrypto@2.0.4':
|
||||||
resolution: {integrity: sha512-1+/5bsjyataf5uUkUNnnVXGRAt+gHVk1KDzozjTqgqJxHvQk1d9fVDohL6CxUhUucTPtu5VR5xNBiV8YCDuGyw==}
|
resolution: {integrity: sha512-1+/5bsjyataf5uUkUNnnVXGRAt+gHVk1KDzozjTqgqJxHvQk1d9fVDohL6CxUhUucTPtu5VR5xNBiV8YCDuGyw==}
|
||||||
|
|
||||||
@@ -1017,8 +1020,8 @@ packages:
|
|||||||
'@push.rocks/smartrx@3.0.10':
|
'@push.rocks/smartrx@3.0.10':
|
||||||
resolution: {integrity: sha512-USjIYcsSfzn14cwOsxgq/bBmWDTTzy3ouWAnW5NdMyRRzEbmeNrvmy6TRqNeDlJ2PsYNTt1rr/zGUqvIy72ITg==}
|
resolution: {integrity: sha512-USjIYcsSfzn14cwOsxgq/bBmWDTTzy3ouWAnW5NdMyRRzEbmeNrvmy6TRqNeDlJ2PsYNTt1rr/zGUqvIy72ITg==}
|
||||||
|
|
||||||
'@push.rocks/smartserve@2.0.1':
|
'@push.rocks/smartserve@2.0.3':
|
||||||
resolution: {integrity: sha512-YQb2qexfCzCqOlLWBBXKMg6xG4zahCPAxomz/KEKAwHtW6wMTtuHKSTSkRTQ0vl9jssLMAmRz2OyafiL9XGJXQ==}
|
resolution: {integrity: sha512-PttdFlh61lsDNSRvRCSlKjRzuxgD3WP2XLuBNXu1hLfqLpQXDESj0ZCRPDZslLZsyFT5aHP9godb4D4L3bzHWA==}
|
||||||
|
|
||||||
'@push.rocks/smartshell@3.3.8':
|
'@push.rocks/smartshell@3.3.8':
|
||||||
resolution: {integrity: sha512-t9J/py0vnea4ZtOs7Anc9dc6lcvg6EDvYBw5eE1mB+KUWxMQf/ROIQwWMo6B9SMNY4JS2UwvfuJQJ8makP/7Tg==}
|
resolution: {integrity: sha512-t9J/py0vnea4ZtOs7Anc9dc6lcvg6EDvYBw5eE1mB+KUWxMQf/ROIQwWMo6B9SMNY4JS2UwvfuJQJ8makP/7Tg==}
|
||||||
@@ -1029,8 +1032,8 @@ packages:
|
|||||||
'@push.rocks/smartstate@2.0.27':
|
'@push.rocks/smartstate@2.0.27':
|
||||||
resolution: {integrity: sha512-q4UKir7GV3hakJWXQR4DoA4tUVwT5GRkJ/MtanHYF0wZLHfS19+nGmyO9y974zk3eT9hmy3+Lq5cKtU2W6+Y3w==}
|
resolution: {integrity: sha512-q4UKir7GV3hakJWXQR4DoA4tUVwT5GRkJ/MtanHYF0wZLHfS19+nGmyO9y974zk3eT9hmy3+Lq5cKtU2W6+Y3w==}
|
||||||
|
|
||||||
'@push.rocks/smartstorage@6.0.1':
|
'@push.rocks/smartstorage@6.3.2':
|
||||||
resolution: {integrity: sha512-W5PEVwO0J2K9YUZRTbKXadC11h6/IBzzqU+P0TIE/xpJZC4K1duEXwEhxGWcbfhCkPRRa51xH8Z5mAmzzm8qxA==}
|
resolution: {integrity: sha512-g8rXlVZ+6iKmzNoybtwQntdb7EWA6WnVmbXNOdwDKWR8w4o/7UMErj+H5mt57iqYIy1pzQAoTb8IWJNsti7XQw==}
|
||||||
|
|
||||||
'@push.rocks/smartstream@3.4.0':
|
'@push.rocks/smartstream@3.4.0':
|
||||||
resolution: {integrity: sha512-kePb44W9n5K96zj2Ms3K4xnYbNXP5AfxDd86zZMDQ1/T10nvkIpL9m5w4lG/VJ4KAsWFs81S87BkkcjhhrY5Kw==}
|
resolution: {integrity: sha512-kePb44W9n5K96zj2Ms3K4xnYbNXP5AfxDd86zZMDQ1/T10nvkIpL9m5w4lG/VJ4KAsWFs81S87BkkcjhhrY5Kw==}
|
||||||
@@ -1050,8 +1053,8 @@ packages:
|
|||||||
'@push.rocks/smartversion@3.0.5':
|
'@push.rocks/smartversion@3.0.5':
|
||||||
resolution: {integrity: sha512-8MZSo1yqyaKxKq0Q5N188l4un++9GFWVbhCAX5mXJwewZHn97ujffTeL+eOQYpWFTEpUhaq1QhL4NhqObBCt1Q==}
|
resolution: {integrity: sha512-8MZSo1yqyaKxKq0Q5N188l4un++9GFWVbhCAX5mXJwewZHn97ujffTeL+eOQYpWFTEpUhaq1QhL4NhqObBCt1Q==}
|
||||||
|
|
||||||
'@push.rocks/smartwatch@6.3.0':
|
'@push.rocks/smartwatch@6.4.0':
|
||||||
resolution: {integrity: sha512-TeZ1PGBoBMpC4/CK8StIj5InEiFfKp7xWJSm3aYMjB/uaoeRP0vXqv1ORIC/TKYGJuEDuAXUsit8tZVjn0qT1Q==}
|
resolution: {integrity: sha512-KDswRgE/siBmZRCsRA07MtW5oF4c9uQEBkwTGPIWneHzksbCDsvs/7agKFEL7WnNifLNwo8w1K1qoiVWkX1fvw==}
|
||||||
engines: {node: '>=20.0.0'}
|
engines: {node: '>=20.0.0'}
|
||||||
|
|
||||||
'@push.rocks/smartyaml@2.0.5':
|
'@push.rocks/smartyaml@2.0.5':
|
||||||
@@ -1083,146 +1086,152 @@ packages:
|
|||||||
resolution: {integrity: sha512-bqorOaGXPOuiOSV81luTKrTghg4O4NBRD0zyv7TIqmrMGf4a0uoozaUMp1X8vQdZW+y0gTzUJP9wkzAE6Cci0g==}
|
resolution: {integrity: sha512-bqorOaGXPOuiOSV81luTKrTghg4O4NBRD0zyv7TIqmrMGf4a0uoozaUMp1X8vQdZW+y0gTzUJP9wkzAE6Cci0g==}
|
||||||
deprecated: This package has been deprecated in favour of the new package at @push.rocks/smartpromise
|
deprecated: This package has been deprecated in favour of the new package at @push.rocks/smartpromise
|
||||||
|
|
||||||
'@rolldown/binding-android-arm64@1.0.0-beta.52':
|
'@rolldown/binding-android-arm64@1.0.0-rc.11':
|
||||||
resolution: {integrity: sha512-MBGIgysimZPqTDcLXI+i9VveijkP5C3EAncEogXhqfax6YXj1Tr2LY3DVuEOMIjWfMPMhtQSPup4fSTAmgjqIw==}
|
resolution: {integrity: sha512-SJ+/g+xNnOh6NqYxD0V3uVN4W3VfnrGsC9/hoglicgTNfABFG9JjISvkkU0dNY84MNHLWyOgxP9v9Y9pX4S7+A==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [android]
|
os: [android]
|
||||||
|
|
||||||
'@rolldown/binding-darwin-arm64@1.0.0-beta.52':
|
'@rolldown/binding-darwin-arm64@1.0.0-rc.11':
|
||||||
resolution: {integrity: sha512-MmKeoLnKu1d9j6r19K8B+prJnIZ7u+zQ+zGQ3YHXGnr41rzE3eqQLovlkvoZnRoxDGPA4ps0pGiwXy6YE3lJyg==}
|
resolution: {integrity: sha512-7WQgR8SfOPwmDZGFkThUvsmd/nwAWv91oCO4I5LS7RKrssPZmOt7jONN0cW17ydGC1n/+puol1IpoieKqQidmg==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@rolldown/binding-darwin-x64@1.0.0-beta.52':
|
'@rolldown/binding-darwin-x64@1.0.0-rc.11':
|
||||||
resolution: {integrity: sha512-qpHedvQBmIjT8zdnjN3nWPR2qjQyJttbXniCEKKdHeAbZG9HyNPBUzQF7AZZGwmS9coQKL+hWg9FhWzh2dZ2IA==}
|
resolution: {integrity: sha512-39Ks6UvIHq4rEogIfQBoBRusj0Q0nPVWIvqmwBLaT6aqQGIakHdESBVOPRRLacy4WwUPIx4ZKzfZ9PMW+IeyUQ==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@rolldown/binding-freebsd-x64@1.0.0-beta.52':
|
'@rolldown/binding-freebsd-x64@1.0.0-rc.11':
|
||||||
resolution: {integrity: sha512-dDp7WbPapj/NVW0LSiH/CLwMhmLwwKb3R7mh2kWX+QW85X1DGVnIEyKh9PmNJjB/+suG1dJygdtdNPVXK1hylg==}
|
resolution: {integrity: sha512-jfsm0ZHfhiqrvWjJAmzsqiIFPz5e7mAoCOPBNTcNgkiid/LaFKiq92+0ojH+nmJmKYkre4t71BWXUZDNp7vsag==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [freebsd]
|
os: [freebsd]
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.52':
|
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.11':
|
||||||
resolution: {integrity: sha512-9e4l6vy5qNSliDPqNfR6CkBOAx6PH7iDV4OJiEJzajajGrVy8gc/IKKJUsoE52G8ud8MX6r3PMl97NfwgOzB7g==}
|
resolution: {integrity: sha512-zjQaUtSyq1nVe3nxmlSCuR96T1LPlpvmJ0SZy0WJFEsV4kFbXcq2u68L4E6O0XeFj4aex9bEauqjW8UQBeAvfQ==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm64-gnu@1.0.0-beta.52':
|
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.11':
|
||||||
resolution: {integrity: sha512-V48oDR84feRU2KRuzpALp594Uqlx27+zFsT6+BgTcXOtu7dWy350J1G28ydoCwKB+oxwsRPx2e7aeQnmd3YJbQ==}
|
resolution: {integrity: sha512-WMW1yE6IOnehTcFE9eipFkm3XN63zypWlrJQ2iF7NrQ9b2LDRjumFoOGJE8RJJTJCTBAdmLMnJ8uVitACUUo1Q==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.52':
|
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.11':
|
||||||
resolution: {integrity: sha512-ENLmSQCWqSA/+YN45V2FqTIemg7QspaiTjlm327eUAMeOLdqmSOVVyrQexJGNTQ5M8sDYCgVAig2Kk01Ggmqaw==}
|
resolution: {integrity: sha512-jfndI9tsfm4APzjNt6QdBkYwre5lRPUgHeDHoI7ydKUuJvz3lZeCfMsI56BZj+7BYqiKsJm7cfd/6KYV7ubrBg==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.52':
|
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.11':
|
||||||
resolution: {integrity: sha512-klahlb2EIFltSUubn/VLjuc3qxp1E7th8ukayPfdkcKvvYcQ5rJztgx8JsJSuAKVzKtNTqUGOhy4On71BuyV8g==}
|
resolution: {integrity: sha512-ZlFgw46NOAGMgcdvdYwAGu2Q+SLFA9LzbJLW+iyMOJyhj5wk6P3KEE9Gct4xWwSzFoPI7JCdYmYMzVtlgQ+zfw==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.11':
|
||||||
|
resolution: {integrity: sha512-hIOYmuT6ofM4K04XAZd3OzMySEO4K0/nc9+jmNcxNAxRi6c5UWpqfw3KMFV4MVFWL+jQsSh+bGw2VqmaPMTLyw==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.11':
|
||||||
|
resolution: {integrity: sha512-qXBQQO9OvkjjQPLdUVr7Nr2t3QTZI7s4KZtfw7HzBgjbmAPSFwSv4rmET9lLSgq3rH/ndA3ngv3Qb8l2njoPNA==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@rolldown/binding-linux-x64-musl@1.0.0-beta.52':
|
'@rolldown/binding-linux-x64-musl@1.0.0-rc.11':
|
||||||
resolution: {integrity: sha512-UuA+JqQIgqtkgGN2c/AQ5wi8M6mJHrahz/wciENPTeI6zEIbbLGoth5XN+sQe2pJDejEVofN9aOAp0kaazwnVg==}
|
resolution: {integrity: sha512-/tpFfoSTzUkH9LPY+cYbqZBDyyX62w5fICq9qzsHLL8uTI6BHip3Q9Uzft0wylk/i8OOwKik8OxW+QAhDmzwmg==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@rolldown/binding-openharmony-arm64@1.0.0-beta.52':
|
'@rolldown/binding-openharmony-arm64@1.0.0-rc.11':
|
||||||
resolution: {integrity: sha512-1BNQW8u4ro8bsN1+tgKENJiqmvc+WfuaUhXzMImOVSMw28pkBKdfZtX2qJPADV3terx+vNJtlsgSGeb3+W6Jiw==}
|
resolution: {integrity: sha512-mcp3Rio2w72IvdZG0oQ4bM2c2oumtwHfUfKncUM6zGgz0KgPz4YmDPQfnXEiY5t3+KD/i8HG2rOB/LxdmieK2g==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [openharmony]
|
os: [openharmony]
|
||||||
|
|
||||||
'@rolldown/binding-wasm32-wasi@1.0.0-beta.52':
|
'@rolldown/binding-wasm32-wasi@1.0.0-rc.11':
|
||||||
resolution: {integrity: sha512-K/p7clhCqJOQpXGykrFaBX2Dp9AUVIDHGc+PtFGBwg7V+mvBTv/tsm3LC3aUmH02H2y3gz4y+nUTQ0MLpofEEg==}
|
resolution: {integrity: sha512-LXk5Hii1Ph9asuGRjBuz8TUxdc1lWzB7nyfdoRgI0WGPZKmCxvlKk8KfYysqtr4MfGElu/f/pEQRh8fcEgkrWw==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
cpu: [wasm32]
|
cpu: [wasm32]
|
||||||
|
|
||||||
'@rolldown/binding-win32-arm64-msvc@1.0.0-beta.52':
|
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.11':
|
||||||
resolution: {integrity: sha512-a4EkXBtnYYsKipjS7QOhEBM4bU5IlR9N1hU+JcVEVeuTiaslIyhWVKsvf7K2YkQHyVAJ+7/A9BtrGqORFcTgng==}
|
resolution: {integrity: sha512-dDwf5otnx0XgRY1yqxOC4ITizcdzS/8cQ3goOWv3jFAo4F+xQYni+hnMuO6+LssHHdJW7+OCVL3CoU4ycnh35Q==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@rolldown/binding-win32-ia32-msvc@1.0.0-beta.52':
|
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.11':
|
||||||
resolution: {integrity: sha512-5ZXcYyd4GxPA6QfbGrNcQjmjbuLGvfz6728pZMsQvGHI+06LT06M6TPtXvFvLgXtexc+OqvFe1yAIXJU1gob/w==}
|
resolution: {integrity: sha512-LN4/skhSggybX71ews7dAj6r2geaMJfm3kMbK2KhFMg9B10AZXnKoLCVVgzhMHL0S+aKtr4p8QbAW8k+w95bAA==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
|
||||||
cpu: [ia32]
|
|
||||||
os: [win32]
|
|
||||||
|
|
||||||
'@rolldown/binding-win32-x64-msvc@1.0.0-beta.52':
|
|
||||||
resolution: {integrity: sha512-tzpnRQXJrSzb8Z9sm97UD3cY0toKOImx+xRKsDLX4zHaAlRXWh7jbaKBePJXEN7gNw7Nm03PBNwphdtA8KSUYQ==}
|
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@rolldown/pluginutils@1.0.0-beta.52':
|
'@rolldown/pluginutils@1.0.0-rc.11':
|
||||||
resolution: {integrity: sha512-/L0htLJZbaZFL1g9OHOblTxbCYIGefErJjtYOwgl9ZqNx27P3L0SDfjhhHIss32gu5NWgnxuT2a2Hnnv6QGHKA==}
|
resolution: {integrity: sha512-xQO9vbwBecJRv9EUcQ/y0dzSTJgA7Q6UVN7xp6B81+tBGSLVAK03yJ9NkJaUA7JFD91kbjxRSC/mDnmvXzbHoQ==}
|
||||||
|
|
||||||
'@rspack/binding-darwin-arm64@1.7.9':
|
'@rspack/binding-darwin-arm64@1.7.10':
|
||||||
resolution: {integrity: sha512-64dgstte0If5czi9bA/cpOe0ryY6wC9AIQRtyJ3DlOF6Tt+y9cKkmUoGu3V+WYaYIZRT7HNk8V7kL8amVjFTYw==}
|
resolution: {integrity: sha512-bsXi7I6TpH+a4L6okIUh1JDvwT+XcK/L7Yvhu5G2t5YYyd2fl5vMM5O9cePRpEb0RdqJZ3Z8i9WIWHap9aQ8Gw==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@rspack/binding-darwin-x64@1.7.9':
|
'@rspack/binding-darwin-x64@1.7.10':
|
||||||
resolution: {integrity: sha512-2QSLs3w4rLy4UUGVnIlkt6IlIKOzR1e0RPsq2FYQW6s3p9JrwRCtOeHohyh7EJSqF54dtfhe9UZSAwba3LqH1Q==}
|
resolution: {integrity: sha512-h/kOGL1bUflDDYnbiUjaRE9kagJpour4FatGihueV03+cRGQ6jpde+BjUakqzMx65CeDbeYI6jAiPhElnlAtRw==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@rspack/binding-linux-arm64-gnu@1.7.9':
|
'@rspack/binding-linux-arm64-gnu@1.7.10':
|
||||||
resolution: {integrity: sha512-qhUGI/uVfvLmKWts4QkVHGL8yfUyJkblZs+OFD5Upa2y676EOsbQgWsCwX4xGB6Tv+TOzFP0SLh/UfO8ZfdE+w==}
|
resolution: {integrity: sha512-Z4reus7UxGM4+JuhiIht8KuGP1KgM7nNhOlXUHcQCMswP/Rymj5oJQN3TDWgijFUZs09ULl8t3T+AQAVTd/WvA==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@rspack/binding-linux-arm64-musl@1.7.9':
|
'@rspack/binding-linux-arm64-musl@1.7.10':
|
||||||
resolution: {integrity: sha512-VjfmR1hgO9n3L6MaE5KG+DXSrrLVqHHOkVcOtS2LMq3bjMTwbBywY7ycymcLnX5KJsol8d3ZGYep6IfSOt3lFA==}
|
resolution: {integrity: sha512-LYaoVmWizG4oQ3g+St3eM5qxsyfH07kLirP7NJcDMgvu3eQ29MeyTZ3ugkgW6LvlmJue7eTQyf6CZlanoF5SSg==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@rspack/binding-linux-x64-gnu@1.7.9':
|
'@rspack/binding-linux-x64-gnu@1.7.10':
|
||||||
resolution: {integrity: sha512-0kldV+3WTs/VYDWzxJ7K40hCW26IHtnk8xPK3whKoo1649rgeXXa0EdsU5P7hG8Ef5SWQjHHHZ/fuHYSO3Y6HA==}
|
resolution: {integrity: sha512-aIm2G4Kcm3qxDTNqKarK0oaLY2iXnCmpRQQhAcMlR0aS2LmxL89XzVeRr9GFA1MzGrAsZONWCLkxQvn3WUbm4Q==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@rspack/binding-linux-x64-musl@1.7.9':
|
'@rspack/binding-linux-x64-musl@1.7.10':
|
||||||
resolution: {integrity: sha512-Gi4872cFtc2d83FKATR6Qcf2VBa/tFCqffI/IwRRl6Hx5FulEBqx+tH7gAuRVF693vrbXNxK+FQ+k4iEsEJxrw==}
|
resolution: {integrity: sha512-SIHQbAgB9IPH0H3H+i5rN5jo9yA/yTMq8b7XfRkTMvZ7P7MXxJ0dE8EJu3BmCLM19sqnTc2eX+SVfE8ZMDzghA==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@rspack/binding-wasm32-wasi@1.7.9':
|
'@rspack/binding-wasm32-wasi@1.7.10':
|
||||||
resolution: {integrity: sha512-5QEzqo6EaolpuZmK6w/mgSueorgGnnzp7dJaAvBj6ECFIg/aLXhXXmWCWbxt7Ws2gKvG5/PgaxDqbUxYL51juA==}
|
resolution: {integrity: sha512-J9HDXHD1tj+9FmX4+K3CTkO7dCE2bootlR37YuC2Owc0Lwl1/i2oGT71KHnMqI9faF/hipAaQM5OywkiiuNB7w==}
|
||||||
cpu: [wasm32]
|
cpu: [wasm32]
|
||||||
|
|
||||||
'@rspack/binding-win32-arm64-msvc@1.7.9':
|
'@rspack/binding-win32-arm64-msvc@1.7.10':
|
||||||
resolution: {integrity: sha512-MMqvcrIc8aOqTuHjWkjdzilvoZ3Hv07Od0Foogiyq3JMudsS3Wcmh7T1dFerGg19MOJcRUeEkrg2NQOMOQ6xDA==}
|
resolution: {integrity: sha512-FaQGSCXH89nMOYW0bVp0bKQDQbrOEFFm7yedla7g6mkWlFVQo5UyBxid5wJUCqGJBtJepRxeRfByWiaI5nVGvg==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@rspack/binding-win32-ia32-msvc@1.7.9':
|
'@rspack/binding-win32-ia32-msvc@1.7.10':
|
||||||
resolution: {integrity: sha512-4kYYS+NZ2CuNbKjq40yB/UEyB51o1PHj5wpr+Y943oOJXpEKWU2Q4vkF8VEohPEcnA9cKVotYCnqStme+02suA==}
|
resolution: {integrity: sha512-/66TNLOeM4R5dHhRWRVbMTgWghgxz+32ym0c/zGGXQRoMbz7210EoL40ALUgdBdeeREO8LoV+Mn7v8/QZCwHzw==}
|
||||||
cpu: [ia32]
|
cpu: [ia32]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@rspack/binding-win32-x64-msvc@1.7.9':
|
'@rspack/binding-win32-x64-msvc@1.7.10':
|
||||||
resolution: {integrity: sha512-1g+QyXXvs+838Un/4GaUvJfARDGHMCs15eXDYWBl5m/Skubyng8djWAgr6ag1+cVoJZXCPOvybTItcblWF3gbQ==}
|
resolution: {integrity: sha512-SUa3v1W7PGFCy6AHRmDsm43/tkfaZFi1TN2oIk5aCdT9T51baDVBjAbehRDu9xFbK4piL3k7uqIVSIrKgVqk1g==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@rspack/binding@1.7.9':
|
'@rspack/binding@1.7.10':
|
||||||
resolution: {integrity: sha512-A56e0NdfNwbOSJoilMkxzaPuVYaKCNn1shuiwWnCIBmhV9ix1n9S1XvquDjkGyv+gCdR1+zfJBOa5DMB7htLHw==}
|
resolution: {integrity: sha512-j+DPEaSJLRgasxXNpYQpvC7wUkQF5WoWPiTfm4fLczwlAmYwGSVkJiyWDrOlvVPiGGYiXIaXEjVWTw6fT6/vnA==}
|
||||||
|
|
||||||
'@rspack/core@1.7.9':
|
'@rspack/core@1.7.10':
|
||||||
resolution: {integrity: sha512-VHuSKvRkuv42Ya+TxEGO0LE0r9+8P4tKGokmomj4R1f/Nu2vtS3yoaIMfC4fR6VuHGd3MZ+KTI0cNNwHfFcskw==}
|
resolution: {integrity: sha512-dO7J0aHSa9Fg2kGT0+ZsM500lMdlNIyCHavIaz7dTDn6KXvFz1qbWQ/48x3OlNFw1mA0jxAjjw9e7h3sWQZUNg==}
|
||||||
engines: {node: '>=18.12.0'}
|
engines: {node: '>=18.12.0'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@swc/helpers': '>=0.5.1'
|
'@swc/helpers': '>=0.5.1'
|
||||||
@@ -2933,8 +2942,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
|
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
rolldown@1.0.0-beta.52:
|
rolldown@1.0.0-rc.11:
|
||||||
resolution: {integrity: sha512-Hbnpljue+JhMJrlOjQ1ixp9me7sUec7OjFvS+A1Qm8k8Xyxmw3ZhxFu7LlSXW1s9AX3POE9W9o2oqCEeR5uDmg==}
|
resolution: {integrity: sha512-NRjoKMusSjfRbSYiH3VSumlkgFe7kYAa3pzVOsVYVFY3zb5d7nS+a3KGQ7hJKXuYWbzJKPVQ9Wxq2UvyK+ENpw==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@@ -2995,8 +3004,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
|
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
|
||||||
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
|
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
|
||||||
|
|
||||||
smol-toml@1.6.0:
|
smol-toml@1.6.1:
|
||||||
resolution: {integrity: sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==}
|
resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==}
|
||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
socks-proxy-agent@8.0.5:
|
socks-proxy-agent@8.0.5:
|
||||||
@@ -3162,8 +3171,8 @@ packages:
|
|||||||
typed-query-selector@2.12.1:
|
typed-query-selector@2.12.1:
|
||||||
resolution: {integrity: sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==}
|
resolution: {integrity: sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==}
|
||||||
|
|
||||||
typescript@5.9.3:
|
typescript@6.0.2:
|
||||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
resolution: {integrity: sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==}
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@@ -3275,6 +3284,18 @@ packages:
|
|||||||
utf-8-validate:
|
utf-8-validate:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
ws@8.20.0:
|
||||||
|
resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==}
|
||||||
|
engines: {node: '>=10.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
bufferutil: ^4.0.1
|
||||||
|
utf-8-validate: '>=5.0.2'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
bufferutil:
|
||||||
|
optional: true
|
||||||
|
utf-8-validate:
|
||||||
|
optional: true
|
||||||
|
|
||||||
xml-parse-from-string@1.0.1:
|
xml-parse-from-string@1.0.1:
|
||||||
resolution: {integrity: sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==}
|
resolution: {integrity: sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==}
|
||||||
|
|
||||||
@@ -3938,9 +3959,9 @@ snapshots:
|
|||||||
'@esbuild/win32-x64@0.27.4':
|
'@esbuild/win32-x64@0.27.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@git.zone/tsbuild@4.3.0':
|
'@git.zone/tsbuild@4.4.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@git.zone/tspublish': 1.11.2
|
'@git.zone/tspublish': 1.11.5
|
||||||
'@push.rocks/early': 4.0.4
|
'@push.rocks/early': 4.0.4
|
||||||
'@push.rocks/smartcli': 4.0.20
|
'@push.rocks/smartcli': 4.0.20
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
@@ -3949,7 +3970,7 @@ snapshots:
|
|||||||
'@push.rocks/smartlog': 3.2.1
|
'@push.rocks/smartlog': 3.2.1
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
typescript: 5.9.3
|
typescript: 6.0.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@nuxt/kit'
|
- '@nuxt/kit'
|
||||||
- aws-crt
|
- aws-crt
|
||||||
@@ -3960,11 +3981,11 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
'@git.zone/tsbundle@2.9.1':
|
'@git.zone/tsbundle@2.10.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/early': 4.0.4
|
'@push.rocks/early': 4.0.4
|
||||||
'@push.rocks/npmextra': 5.3.3
|
|
||||||
'@push.rocks/smartcli': 4.0.20
|
'@push.rocks/smartcli': 4.0.20
|
||||||
|
'@push.rocks/smartconfig': 6.1.0
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartfs': 1.5.0
|
'@push.rocks/smartfs': 1.5.0
|
||||||
'@push.rocks/smartinteract': 2.0.16
|
'@push.rocks/smartinteract': 2.0.16
|
||||||
@@ -3973,12 +3994,12 @@ snapshots:
|
|||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartspawn': 3.0.3
|
'@push.rocks/smartspawn': 3.0.3
|
||||||
'@rspack/core': 1.7.9
|
'@rspack/core': 1.7.10
|
||||||
'@types/html-minifier': 4.0.6
|
'@types/html-minifier': 4.0.6
|
||||||
esbuild: 0.27.4
|
esbuild: 0.27.4
|
||||||
html-minifier: 4.0.0
|
html-minifier: 4.0.0
|
||||||
rolldown: 1.0.0-beta.52
|
rolldown: 1.0.0-rc.11
|
||||||
typescript: 5.9.3
|
typescript: 6.0.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@nuxt/kit'
|
- '@nuxt/kit'
|
||||||
- '@swc/helpers'
|
- '@swc/helpers'
|
||||||
@@ -3986,11 +4007,11 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
'@git.zone/tspublish@1.11.2':
|
'@git.zone/tspublish@1.11.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/consolecolor': 2.0.3
|
'@push.rocks/consolecolor': 2.0.3
|
||||||
'@push.rocks/npmextra': 5.3.3
|
|
||||||
'@push.rocks/smartcli': 4.0.20
|
'@push.rocks/smartcli': 4.0.20
|
||||||
|
'@push.rocks/smartconfig': 6.1.0
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartfile': 13.1.2
|
'@push.rocks/smartfile': 13.1.2
|
||||||
'@push.rocks/smartfs': 1.5.0
|
'@push.rocks/smartfs': 1.5.0
|
||||||
@@ -4009,34 +4030,34 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
'@git.zone/tsrun@2.0.1':
|
'@git.zone/tsrun@2.0.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartfile': 13.1.2
|
'@push.rocks/smartfile': 13.1.2
|
||||||
'@push.rocks/smartshell': 3.3.8
|
'@push.rocks/smartshell': 3.3.8
|
||||||
tsx: 4.21.0
|
tsx: 4.21.0
|
||||||
|
|
||||||
'@git.zone/tsrust@1.3.0':
|
'@git.zone/tsrust@1.3.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/early': 4.0.4
|
'@push.rocks/early': 4.0.4
|
||||||
'@push.rocks/npmextra': 5.3.3
|
|
||||||
'@push.rocks/smartcli': 4.0.20
|
'@push.rocks/smartcli': 4.0.20
|
||||||
|
'@push.rocks/smartconfig': 6.1.0
|
||||||
'@push.rocks/smartfile': 13.1.2
|
'@push.rocks/smartfile': 13.1.2
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartshell': 3.3.8
|
'@push.rocks/smartshell': 3.3.8
|
||||||
smol-toml: 1.6.0
|
smol-toml: 1.6.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@nuxt/kit'
|
- '@nuxt/kit'
|
||||||
- react
|
- react
|
||||||
- supports-color
|
- supports-color
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
'@git.zone/tstest@3.5.0(socks@2.8.7)(typescript@5.9.3)':
|
'@git.zone/tstest@3.6.0(socks@2.8.7)(typescript@6.0.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@git.zone/tsbundle': 2.9.1
|
'@git.zone/tsbundle': 2.10.0
|
||||||
'@git.zone/tsrun': 2.0.1
|
'@git.zone/tsrun': 2.0.2
|
||||||
'@push.rocks/consolecolor': 2.0.3
|
'@push.rocks/consolecolor': 2.0.3
|
||||||
'@push.rocks/qenv': 6.1.3
|
'@push.rocks/qenv': 6.1.3
|
||||||
'@push.rocks/smartbrowser': 2.0.11(typescript@5.9.3)
|
'@push.rocks/smartbrowser': 2.0.11(typescript@6.0.2)
|
||||||
'@push.rocks/smartcrypto': 2.0.4
|
'@push.rocks/smartcrypto': 2.0.4
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartenv': 6.0.0
|
'@push.rocks/smartenv': 6.0.0
|
||||||
@@ -4050,14 +4071,14 @@ snapshots:
|
|||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartrequest': 5.0.1
|
'@push.rocks/smartrequest': 5.0.1
|
||||||
'@push.rocks/smartserve': 2.0.1
|
'@push.rocks/smartserve': 2.0.3
|
||||||
'@push.rocks/smartshell': 3.3.8
|
'@push.rocks/smartshell': 3.3.8
|
||||||
'@push.rocks/smartstorage': 6.0.1
|
'@push.rocks/smartstorage': 6.3.2
|
||||||
'@push.rocks/smarttime': 4.2.3
|
'@push.rocks/smarttime': 4.2.3
|
||||||
'@push.rocks/smartwatch': 6.3.0
|
'@push.rocks/smartwatch': 6.4.0
|
||||||
'@types/ws': 8.18.1
|
'@types/ws': 8.18.1
|
||||||
figures: 6.1.0
|
figures: 6.1.0
|
||||||
ws: 8.19.0
|
ws: 8.20.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@aws-sdk/credential-providers'
|
- '@aws-sdk/credential-providers'
|
||||||
- '@mongodb-js/zstd'
|
- '@mongodb-js/zstd'
|
||||||
@@ -4513,7 +4534,7 @@ snapshots:
|
|||||||
'@tybys/wasm-util': 0.10.1
|
'@tybys/wasm-util': 0.10.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@oxc-project/types@0.99.0': {}
|
'@oxc-project/types@0.122.0': {}
|
||||||
|
|
||||||
'@pdf-lib/standard-fonts@1.0.0':
|
'@pdf-lib/standard-fonts@1.0.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -4684,6 +4705,15 @@ snapshots:
|
|||||||
'@types/symbol-tree': 3.2.5
|
'@types/symbol-tree': 3.2.5
|
||||||
symbol-tree: 3.2.4
|
symbol-tree: 3.2.4
|
||||||
|
|
||||||
|
'@push.rocks/lik@6.4.0':
|
||||||
|
dependencies:
|
||||||
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
|
'@push.rocks/smartmatch': 2.0.0
|
||||||
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
|
'@push.rocks/smartrx': 3.0.10
|
||||||
|
'@push.rocks/smarttime': 4.2.3
|
||||||
|
symbol-tree: 3.2.4
|
||||||
|
|
||||||
'@push.rocks/mongodump@1.1.0(socks@2.8.7)':
|
'@push.rocks/mongodump@1.1.0(socks@2.8.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/lik': 6.3.1
|
'@push.rocks/lik': 6.3.1
|
||||||
@@ -4702,23 +4732,6 @@ snapshots:
|
|||||||
- snappy
|
- snappy
|
||||||
- socks
|
- socks
|
||||||
|
|
||||||
'@push.rocks/npmextra@5.3.3':
|
|
||||||
dependencies:
|
|
||||||
'@push.rocks/qenv': 6.1.3
|
|
||||||
'@push.rocks/smartfile': 11.2.7
|
|
||||||
'@push.rocks/smartjson': 5.2.0
|
|
||||||
'@push.rocks/smartlog': 3.2.1
|
|
||||||
'@push.rocks/smartpath': 6.0.0
|
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
|
||||||
'@push.rocks/smartrx': 3.0.10
|
|
||||||
'@push.rocks/taskbuffer': 3.5.0
|
|
||||||
'@tsclass/tsclass': 9.5.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- '@nuxt/kit'
|
|
||||||
- react
|
|
||||||
- supports-color
|
|
||||||
- vue
|
|
||||||
|
|
||||||
'@push.rocks/qenv@6.1.3':
|
'@push.rocks/qenv@6.1.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedrequest': 3.2.5
|
'@api.global/typedrequest': 3.2.5
|
||||||
@@ -4748,11 +4761,11 @@ snapshots:
|
|||||||
- react-native-b4a
|
- react-native-b4a
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@push.rocks/smartbrowser@2.0.11(typescript@5.9.3)':
|
'@push.rocks/smartbrowser@2.0.11(typescript@6.0.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartpdf': 4.2.0(typescript@5.9.3)
|
'@push.rocks/smartpdf': 4.2.0(typescript@6.0.2)
|
||||||
'@push.rocks/smartpuppeteer': 2.0.5(typescript@5.9.3)
|
'@push.rocks/smartpuppeteer': 2.0.5(typescript@6.0.2)
|
||||||
'@push.rocks/smartunique': 3.0.9
|
'@push.rocks/smartunique': 3.0.9
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@nuxt/kit'
|
- '@nuxt/kit'
|
||||||
@@ -4811,6 +4824,23 @@ snapshots:
|
|||||||
'@push.rocks/smarturl': 3.1.0
|
'@push.rocks/smarturl': 3.1.0
|
||||||
'@push.rocks/webrequest': 4.0.5
|
'@push.rocks/webrequest': 4.0.5
|
||||||
|
|
||||||
|
'@push.rocks/smartconfig@6.1.0':
|
||||||
|
dependencies:
|
||||||
|
'@push.rocks/qenv': 6.1.3
|
||||||
|
'@push.rocks/smartfile': 11.2.7
|
||||||
|
'@push.rocks/smartjson': 5.2.0
|
||||||
|
'@push.rocks/smartlog': 3.2.1
|
||||||
|
'@push.rocks/smartpath': 6.0.0
|
||||||
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
|
'@push.rocks/smartrx': 3.0.10
|
||||||
|
'@push.rocks/taskbuffer': 3.5.0
|
||||||
|
'@tsclass/tsclass': 9.5.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@nuxt/kit'
|
||||||
|
- react
|
||||||
|
- supports-color
|
||||||
|
- vue
|
||||||
|
|
||||||
'@push.rocks/smartcrypto@2.0.4':
|
'@push.rocks/smartcrypto@2.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
@@ -5131,7 +5161,7 @@ snapshots:
|
|||||||
|
|
||||||
'@push.rocks/smartpath@6.0.0': {}
|
'@push.rocks/smartpath@6.0.0': {}
|
||||||
|
|
||||||
'@push.rocks/smartpdf@4.2.0(typescript@5.9.3)':
|
'@push.rocks/smartpdf@4.2.0(typescript@6.0.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartbuffer': 3.0.5
|
'@push.rocks/smartbuffer': 3.0.5
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
@@ -5140,8 +5170,8 @@ snapshots:
|
|||||||
'@push.rocks/smartnetwork': 4.4.0
|
'@push.rocks/smartnetwork': 4.4.0
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
'@push.rocks/smartpuppeteer': 2.0.5(typescript@5.9.3)
|
'@push.rocks/smartpuppeteer': 2.0.5(typescript@6.0.2)
|
||||||
'@push.rocks/smartserve': 2.0.1
|
'@push.rocks/smartserve': 2.0.3
|
||||||
'@push.rocks/smartunique': 3.0.9
|
'@push.rocks/smartunique': 3.0.9
|
||||||
'@tsclass/tsclass': 9.5.0
|
'@tsclass/tsclass': 9.5.0
|
||||||
pdf-lib: 1.17.1
|
pdf-lib: 1.17.1
|
||||||
@@ -5166,11 +5196,11 @@ snapshots:
|
|||||||
|
|
||||||
'@push.rocks/smartpromise@4.2.3': {}
|
'@push.rocks/smartpromise@4.2.3': {}
|
||||||
|
|
||||||
'@push.rocks/smartpuppeteer@2.0.5(typescript@5.9.3)':
|
'@push.rocks/smartpuppeteer@2.0.5(typescript@6.0.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartshell': 3.3.8
|
'@push.rocks/smartshell': 3.3.8
|
||||||
puppeteer: 24.40.0(typescript@5.9.3)
|
puppeteer: 24.40.0(typescript@6.0.2)
|
||||||
tree-kill: 1.2.2
|
tree-kill: 1.2.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bare-abort-controller
|
- bare-abort-controller
|
||||||
@@ -5221,7 +5251,7 @@ snapshots:
|
|||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
rxjs: 7.8.2
|
rxjs: 7.8.2
|
||||||
|
|
||||||
'@push.rocks/smartserve@2.0.1':
|
'@push.rocks/smartserve@2.0.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@api.global/typedrequest': 3.2.5
|
'@api.global/typedrequest': 3.2.5
|
||||||
'@cfworker/json-schema': 4.1.1
|
'@cfworker/json-schema': 4.1.1
|
||||||
@@ -5260,7 +5290,7 @@ snapshots:
|
|||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
'@push.rocks/webstore': 2.0.20
|
'@push.rocks/webstore': 2.0.20
|
||||||
|
|
||||||
'@push.rocks/smartstorage@6.0.1':
|
'@push.rocks/smartstorage@6.3.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartpath': 6.0.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartrust': 1.3.2
|
'@push.rocks/smartrust': 1.3.2
|
||||||
@@ -5301,11 +5331,12 @@ snapshots:
|
|||||||
'@types/semver': 7.7.1
|
'@types/semver': 7.7.1
|
||||||
semver: 7.7.4
|
semver: 7.7.4
|
||||||
|
|
||||||
'@push.rocks/smartwatch@6.3.0':
|
'@push.rocks/smartwatch@6.4.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/lik': 6.3.1
|
'@push.rocks/lik': 6.4.0
|
||||||
'@push.rocks/smartenv': 6.0.0
|
'@push.rocks/smartenv': 6.0.0
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
|
'@push.rocks/smartrust': 1.3.2
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
chokidar: 5.0.0
|
chokidar: 5.0.0
|
||||||
picomatch: 4.0.3
|
picomatch: 4.0.3
|
||||||
@@ -5374,101 +5405,104 @@ snapshots:
|
|||||||
|
|
||||||
'@pushrocks/smartpromise@4.0.2': {}
|
'@pushrocks/smartpromise@4.0.2': {}
|
||||||
|
|
||||||
'@rolldown/binding-android-arm64@1.0.0-beta.52':
|
'@rolldown/binding-android-arm64@1.0.0-rc.11':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-darwin-arm64@1.0.0-beta.52':
|
'@rolldown/binding-darwin-arm64@1.0.0-rc.11':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-darwin-x64@1.0.0-beta.52':
|
'@rolldown/binding-darwin-x64@1.0.0-rc.11':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-freebsd-x64@1.0.0-beta.52':
|
'@rolldown/binding-freebsd-x64@1.0.0-rc.11':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.52':
|
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.11':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm64-gnu@1.0.0-beta.52':
|
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.11':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm64-musl@1.0.0-beta.52':
|
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.11':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-linux-x64-gnu@1.0.0-beta.52':
|
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.11':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-linux-x64-musl@1.0.0-beta.52':
|
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.11':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-openharmony-arm64@1.0.0-beta.52':
|
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.11':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-wasm32-wasi@1.0.0-beta.52':
|
'@rolldown/binding-linux-x64-musl@1.0.0-rc.11':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-openharmony-arm64@1.0.0-rc.11':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-wasm32-wasi@1.0.0-rc.11':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@napi-rs/wasm-runtime': 1.1.1
|
'@napi-rs/wasm-runtime': 1.1.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-win32-arm64-msvc@1.0.0-beta.52':
|
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.11':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-win32-ia32-msvc@1.0.0-beta.52':
|
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.11':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-win32-x64-msvc@1.0.0-beta.52':
|
'@rolldown/pluginutils@1.0.0-rc.11': {}
|
||||||
|
|
||||||
|
'@rspack/binding-darwin-arm64@1.7.10':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/pluginutils@1.0.0-beta.52': {}
|
'@rspack/binding-darwin-x64@1.7.10':
|
||||||
|
|
||||||
'@rspack/binding-darwin-arm64@1.7.9':
|
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rspack/binding-darwin-x64@1.7.9':
|
'@rspack/binding-linux-arm64-gnu@1.7.10':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rspack/binding-linux-arm64-gnu@1.7.9':
|
'@rspack/binding-linux-arm64-musl@1.7.10':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rspack/binding-linux-arm64-musl@1.7.9':
|
'@rspack/binding-linux-x64-gnu@1.7.10':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rspack/binding-linux-x64-gnu@1.7.9':
|
'@rspack/binding-linux-x64-musl@1.7.10':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rspack/binding-linux-x64-musl@1.7.9':
|
'@rspack/binding-wasm32-wasi@1.7.10':
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@rspack/binding-wasm32-wasi@1.7.9':
|
|
||||||
dependencies:
|
dependencies:
|
||||||
'@napi-rs/wasm-runtime': 1.0.7
|
'@napi-rs/wasm-runtime': 1.0.7
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rspack/binding-win32-arm64-msvc@1.7.9':
|
'@rspack/binding-win32-arm64-msvc@1.7.10':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rspack/binding-win32-ia32-msvc@1.7.9':
|
'@rspack/binding-win32-ia32-msvc@1.7.10':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rspack/binding-win32-x64-msvc@1.7.9':
|
'@rspack/binding-win32-x64-msvc@1.7.10':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rspack/binding@1.7.9':
|
'@rspack/binding@1.7.10':
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@rspack/binding-darwin-arm64': 1.7.9
|
'@rspack/binding-darwin-arm64': 1.7.10
|
||||||
'@rspack/binding-darwin-x64': 1.7.9
|
'@rspack/binding-darwin-x64': 1.7.10
|
||||||
'@rspack/binding-linux-arm64-gnu': 1.7.9
|
'@rspack/binding-linux-arm64-gnu': 1.7.10
|
||||||
'@rspack/binding-linux-arm64-musl': 1.7.9
|
'@rspack/binding-linux-arm64-musl': 1.7.10
|
||||||
'@rspack/binding-linux-x64-gnu': 1.7.9
|
'@rspack/binding-linux-x64-gnu': 1.7.10
|
||||||
'@rspack/binding-linux-x64-musl': 1.7.9
|
'@rspack/binding-linux-x64-musl': 1.7.10
|
||||||
'@rspack/binding-wasm32-wasi': 1.7.9
|
'@rspack/binding-wasm32-wasi': 1.7.10
|
||||||
'@rspack/binding-win32-arm64-msvc': 1.7.9
|
'@rspack/binding-win32-arm64-msvc': 1.7.10
|
||||||
'@rspack/binding-win32-ia32-msvc': 1.7.9
|
'@rspack/binding-win32-ia32-msvc': 1.7.10
|
||||||
'@rspack/binding-win32-x64-msvc': 1.7.9
|
'@rspack/binding-win32-x64-msvc': 1.7.10
|
||||||
|
|
||||||
'@rspack/core@1.7.9':
|
'@rspack/core@1.7.10':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@module-federation/runtime-tools': 0.22.0
|
'@module-federation/runtime-tools': 0.22.0
|
||||||
'@rspack/binding': 1.7.9
|
'@rspack/binding': 1.7.10
|
||||||
'@rspack/lite-tapable': 1.1.0
|
'@rspack/lite-tapable': 1.1.0
|
||||||
|
|
||||||
'@rspack/lite-tapable@1.1.0': {}
|
'@rspack/lite-tapable@1.1.0': {}
|
||||||
@@ -6191,14 +6225,14 @@ snapshots:
|
|||||||
ini: 1.3.8
|
ini: 1.3.8
|
||||||
proto-list: 1.2.4
|
proto-list: 1.2.4
|
||||||
|
|
||||||
cosmiconfig@9.0.1(typescript@5.9.3):
|
cosmiconfig@9.0.1(typescript@6.0.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
env-paths: 2.2.1
|
env-paths: 2.2.1
|
||||||
import-fresh: 3.3.1
|
import-fresh: 3.3.1
|
||||||
js-yaml: 4.1.1
|
js-yaml: 4.1.1
|
||||||
parse-json: 5.2.0
|
parse-json: 5.2.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.9.3
|
typescript: 6.0.2
|
||||||
|
|
||||||
croner@10.0.1: {}
|
croner@10.0.1: {}
|
||||||
|
|
||||||
@@ -7438,7 +7472,7 @@ snapshots:
|
|||||||
devtools-protocol: 0.0.1581282
|
devtools-protocol: 0.0.1581282
|
||||||
typed-query-selector: 2.12.1
|
typed-query-selector: 2.12.1
|
||||||
webdriver-bidi-protocol: 0.4.1
|
webdriver-bidi-protocol: 0.4.1
|
||||||
ws: 8.19.0
|
ws: 8.20.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bare-abort-controller
|
- bare-abort-controller
|
||||||
- bare-buffer
|
- bare-buffer
|
||||||
@@ -7447,11 +7481,11 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
|
||||||
puppeteer@24.40.0(typescript@5.9.3):
|
puppeteer@24.40.0(typescript@6.0.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@puppeteer/browsers': 2.13.0
|
'@puppeteer/browsers': 2.13.0
|
||||||
chromium-bidi: 14.0.0(devtools-protocol@0.0.1581282)
|
chromium-bidi: 14.0.0(devtools-protocol@0.0.1581282)
|
||||||
cosmiconfig: 9.0.1(typescript@5.9.3)
|
cosmiconfig: 9.0.1(typescript@6.0.2)
|
||||||
devtools-protocol: 0.0.1581282
|
devtools-protocol: 0.0.1581282
|
||||||
puppeteer-core: 24.40.0
|
puppeteer-core: 24.40.0
|
||||||
typed-query-selector: 2.12.1
|
typed-query-selector: 2.12.1
|
||||||
@@ -7570,25 +7604,26 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
glob: 7.2.3
|
glob: 7.2.3
|
||||||
|
|
||||||
rolldown@1.0.0-beta.52:
|
rolldown@1.0.0-rc.11:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@oxc-project/types': 0.99.0
|
'@oxc-project/types': 0.122.0
|
||||||
'@rolldown/pluginutils': 1.0.0-beta.52
|
'@rolldown/pluginutils': 1.0.0-rc.11
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@rolldown/binding-android-arm64': 1.0.0-beta.52
|
'@rolldown/binding-android-arm64': 1.0.0-rc.11
|
||||||
'@rolldown/binding-darwin-arm64': 1.0.0-beta.52
|
'@rolldown/binding-darwin-arm64': 1.0.0-rc.11
|
||||||
'@rolldown/binding-darwin-x64': 1.0.0-beta.52
|
'@rolldown/binding-darwin-x64': 1.0.0-rc.11
|
||||||
'@rolldown/binding-freebsd-x64': 1.0.0-beta.52
|
'@rolldown/binding-freebsd-x64': 1.0.0-rc.11
|
||||||
'@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.52
|
'@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.11
|
||||||
'@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.52
|
'@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.11
|
||||||
'@rolldown/binding-linux-arm64-musl': 1.0.0-beta.52
|
'@rolldown/binding-linux-arm64-musl': 1.0.0-rc.11
|
||||||
'@rolldown/binding-linux-x64-gnu': 1.0.0-beta.52
|
'@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.11
|
||||||
'@rolldown/binding-linux-x64-musl': 1.0.0-beta.52
|
'@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.11
|
||||||
'@rolldown/binding-openharmony-arm64': 1.0.0-beta.52
|
'@rolldown/binding-linux-x64-gnu': 1.0.0-rc.11
|
||||||
'@rolldown/binding-wasm32-wasi': 1.0.0-beta.52
|
'@rolldown/binding-linux-x64-musl': 1.0.0-rc.11
|
||||||
'@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.52
|
'@rolldown/binding-openharmony-arm64': 1.0.0-rc.11
|
||||||
'@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.52
|
'@rolldown/binding-wasm32-wasi': 1.0.0-rc.11
|
||||||
'@rolldown/binding-win32-x64-msvc': 1.0.0-beta.52
|
'@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.11
|
||||||
|
'@rolldown/binding-win32-x64-msvc': 1.0.0-rc.11
|
||||||
|
|
||||||
run-async@3.0.0: {}
|
run-async@3.0.0: {}
|
||||||
|
|
||||||
@@ -7660,7 +7695,7 @@ snapshots:
|
|||||||
|
|
||||||
smart-buffer@4.2.0: {}
|
smart-buffer@4.2.0: {}
|
||||||
|
|
||||||
smol-toml@1.6.0: {}
|
smol-toml@1.6.1: {}
|
||||||
|
|
||||||
socks-proxy-agent@8.0.5:
|
socks-proxy-agent@8.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7861,7 +7896,7 @@ snapshots:
|
|||||||
|
|
||||||
typed-query-selector@2.12.1: {}
|
typed-query-selector@2.12.1: {}
|
||||||
|
|
||||||
typescript@5.9.3: {}
|
typescript@6.0.2: {}
|
||||||
|
|
||||||
uglify-js@3.19.3: {}
|
uglify-js@3.19.3: {}
|
||||||
|
|
||||||
@@ -7963,6 +7998,8 @@ snapshots:
|
|||||||
|
|
||||||
ws@8.19.0: {}
|
ws@8.19.0: {}
|
||||||
|
|
||||||
|
ws@8.20.0: {}
|
||||||
|
|
||||||
xml-parse-from-string@1.0.1: {}
|
xml-parse-from-string@1.0.1: {}
|
||||||
|
|
||||||
xml2js@0.5.0:
|
xml2js@0.5.0:
|
||||||
|
|||||||
@@ -1,43 +1,99 @@
|
|||||||
//! Bounded, TTL-based protocol detection cache for backend protocol auto-detection.
|
//! Bounded, sliding-TTL protocol detection cache with periodic re-probing and failure suppression.
|
||||||
//!
|
//!
|
||||||
//! Caches the detected protocol (H1, H2, or H3) per backend endpoint and requested
|
//! Caches the detected protocol (H1, H2, or H3) per backend endpoint and requested
|
||||||
//! domain (host:port + requested_host). This prevents cache oscillation when multiple
|
//! domain (host:port + requested_host). This prevents cache oscillation when multiple
|
||||||
//! frontend domains share the same backend but differ in protocol support.
|
//! frontend domains share the same backend but differ in protocol support.
|
||||||
//!
|
//!
|
||||||
//! H3 detection uses the browser model: Alt-Svc headers from H1/H2 responses are
|
//! ## Sliding TTL
|
||||||
//! parsed and cached, including the advertised H3 port (which may differ from TCP).
|
//!
|
||||||
|
//! Each cache hit refreshes the entry's expiry timer (`last_accessed_at`). Entries
|
||||||
|
//! remain valid for up to 1 day of continuous use. Every 5 minutes, the next request
|
||||||
|
//! triggers an inline ALPN re-probe to verify the cached protocol is still correct.
|
||||||
|
//!
|
||||||
|
//! ## Upgrade signals
|
||||||
|
//!
|
||||||
|
//! - ALPN (TLS handshake) → detects H2 vs H1
|
||||||
|
//! - Alt-Svc (response header) → advertises H3
|
||||||
|
//!
|
||||||
|
//! ## Protocol transitions
|
||||||
|
//!
|
||||||
|
//! All protocol changes are logged at `info!()` level with the reason:
|
||||||
|
//! "Protocol transition: H1 → H2 because periodic ALPN re-probe"
|
||||||
|
//!
|
||||||
|
//! ## Failure suppression
|
||||||
|
//!
|
||||||
|
//! When a protocol fails, `record_failure()` prevents upgrade signals from
|
||||||
|
//! re-introducing it until an escalating cooldown expires (5s → 10s → ... → 300s).
|
||||||
|
//! Within-request escalation is allowed via `can_retry()` after a 5s minimum gap.
|
||||||
|
//!
|
||||||
|
//! ## Total failure eviction
|
||||||
|
//!
|
||||||
|
//! When all protocols (H3, H2, H1) fail for a backend, the cache entry is evicted
|
||||||
|
//! entirely via `evict()`, forcing a fresh probe on the next request.
|
||||||
|
//!
|
||||||
|
//! Cascading: when a lower protocol also fails, higher protocol cooldowns are
|
||||||
|
//! reduced to 5s remaining (not instant clear), preventing tight retry loops.
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use tracing::debug;
|
use tracing::{debug, info};
|
||||||
|
|
||||||
/// TTL for cached protocol detection results.
|
/// Sliding TTL for cached protocol detection results.
|
||||||
/// After this duration, the next request will re-probe the backend.
|
/// Entries that haven't been accessed for this duration are evicted.
|
||||||
const PROTOCOL_CACHE_TTL: Duration = Duration::from_secs(300); // 5 minutes
|
/// Each `get()` call refreshes the timer (sliding window).
|
||||||
|
const PROTOCOL_CACHE_TTL: Duration = Duration::from_secs(86400); // 1 day
|
||||||
|
|
||||||
|
/// Interval between inline ALPN re-probes for H1/H2 entries.
|
||||||
|
/// When a cached entry's `last_probed_at` exceeds this, the next request
|
||||||
|
/// triggers an ALPN re-probe to verify the backend still speaks the same protocol.
|
||||||
|
const PROTOCOL_REPROBE_INTERVAL: Duration = Duration::from_secs(300); // 5 minutes
|
||||||
|
|
||||||
/// Maximum number of entries in the protocol cache.
|
/// Maximum number of entries in the protocol cache.
|
||||||
/// Prevents unbounded growth when backends come and go.
|
|
||||||
const PROTOCOL_CACHE_MAX_ENTRIES: usize = 4096;
|
const PROTOCOL_CACHE_MAX_ENTRIES: usize = 4096;
|
||||||
|
|
||||||
/// Background cleanup interval for the protocol cache.
|
/// Background cleanup interval.
|
||||||
const PROTOCOL_CACHE_CLEANUP_INTERVAL: Duration = Duration::from_secs(60);
|
const PROTOCOL_CACHE_CLEANUP_INTERVAL: Duration = Duration::from_secs(60);
|
||||||
|
|
||||||
|
/// Minimum cooldown between retry attempts of a failed protocol.
|
||||||
|
const PROTOCOL_FAILURE_COOLDOWN: Duration = Duration::from_secs(5);
|
||||||
|
|
||||||
|
/// Maximum cooldown (escalation ceiling).
|
||||||
|
const PROTOCOL_FAILURE_MAX_COOLDOWN: Duration = Duration::from_secs(300);
|
||||||
|
|
||||||
|
/// Consecutive failure count at which cooldown reaches maximum.
|
||||||
|
/// 5s × 2^5 = 160s, 5s × 2^6 = 320s → capped at 300s.
|
||||||
|
const PROTOCOL_FAILURE_ESCALATION_CAP: u32 = 6;
|
||||||
|
|
||||||
/// Detected backend protocol.
|
/// Detected backend protocol.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub enum DetectedProtocol {
|
pub enum DetectedProtocol {
|
||||||
H1,
|
H1,
|
||||||
H2,
|
H2,
|
||||||
H3,
|
H3,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for DetectedProtocol {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
DetectedProtocol::H1 => write!(f, "H1"),
|
||||||
|
DetectedProtocol::H2 => write!(f, "H2"),
|
||||||
|
DetectedProtocol::H3 => write!(f, "H3"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Result of a protocol cache lookup.
|
/// Result of a protocol cache lookup.
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct CachedProtocol {
|
pub struct CachedProtocol {
|
||||||
pub protocol: DetectedProtocol,
|
pub protocol: DetectedProtocol,
|
||||||
/// For H3: the port advertised by Alt-Svc (may differ from TCP port).
|
/// For H3: the port advertised by Alt-Svc (may differ from TCP port).
|
||||||
pub h3_port: Option<u16>,
|
pub h3_port: Option<u16>,
|
||||||
|
/// True if the entry's `last_probed_at` exceeds `PROTOCOL_REPROBE_INTERVAL`.
|
||||||
|
/// Caller should perform an inline ALPN re-probe and call `update_probe_result()`.
|
||||||
|
/// Always `false` for H3 entries (H3 is discovered via Alt-Svc, not ALPN).
|
||||||
|
pub needs_reprobe: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Key for the protocol cache: (host, port, requested_host).
|
/// Key for the protocol cache: (host, port, requested_host).
|
||||||
@@ -50,24 +106,111 @@ pub struct ProtocolCacheKey {
|
|||||||
pub requested_host: Option<String>,
|
pub requested_host: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A cached protocol detection result with a timestamp.
|
/// A cached protocol detection result with timestamps.
|
||||||
struct CachedEntry {
|
struct CachedEntry {
|
||||||
protocol: DetectedProtocol,
|
protocol: DetectedProtocol,
|
||||||
|
/// When this protocol was first detected (or last changed).
|
||||||
detected_at: Instant,
|
detected_at: Instant,
|
||||||
|
/// Last time any request used this entry (sliding-window TTL).
|
||||||
|
last_accessed_at: Instant,
|
||||||
|
/// Last time an ALPN re-probe was performed for this entry.
|
||||||
|
last_probed_at: Instant,
|
||||||
/// For H3: the port advertised by Alt-Svc (may differ from TCP port).
|
/// For H3: the port advertised by Alt-Svc (may differ from TCP port).
|
||||||
h3_port: Option<u16>,
|
h3_port: Option<u16>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bounded, TTL-based protocol detection cache.
|
/// Failure record for a single protocol level.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct FailureRecord {
|
||||||
|
/// When the failure was last recorded.
|
||||||
|
failed_at: Instant,
|
||||||
|
/// Current cooldown duration. Escalates on consecutive failures.
|
||||||
|
cooldown: Duration,
|
||||||
|
/// Number of consecutive failures (for escalation).
|
||||||
|
consecutive_failures: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Per-key failure state. Tracks failures at each upgradeable protocol level.
|
||||||
|
/// H1 is never tracked (it's the protocol floor — nothing to fall back to).
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
struct FailureState {
|
||||||
|
h2: Option<FailureRecord>,
|
||||||
|
h3: Option<FailureRecord>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FailureState {
|
||||||
|
fn is_empty(&self) -> bool {
|
||||||
|
self.h2.is_none() && self.h3.is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn all_expired(&self) -> bool {
|
||||||
|
let h2_expired = self.h2.as_ref()
|
||||||
|
.map(|r| r.failed_at.elapsed() >= r.cooldown)
|
||||||
|
.unwrap_or(true);
|
||||||
|
let h3_expired = self.h3.as_ref()
|
||||||
|
.map(|r| r.failed_at.elapsed() >= r.cooldown)
|
||||||
|
.unwrap_or(true);
|
||||||
|
h2_expired && h3_expired
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(&self, protocol: DetectedProtocol) -> Option<&FailureRecord> {
|
||||||
|
match protocol {
|
||||||
|
DetectedProtocol::H2 => self.h2.as_ref(),
|
||||||
|
DetectedProtocol::H3 => self.h3.as_ref(),
|
||||||
|
DetectedProtocol::H1 => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_mut(&mut self, protocol: DetectedProtocol) -> &mut Option<FailureRecord> {
|
||||||
|
match protocol {
|
||||||
|
DetectedProtocol::H2 => &mut self.h2,
|
||||||
|
DetectedProtocol::H3 => &mut self.h3,
|
||||||
|
DetectedProtocol::H1 => unreachable!("H1 failures are never recorded"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Snapshot of a single protocol cache entry, suitable for metrics/UI display.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ProtocolCacheEntry {
|
||||||
|
pub host: String,
|
||||||
|
pub port: u16,
|
||||||
|
pub domain: Option<String>,
|
||||||
|
pub protocol: String,
|
||||||
|
pub h3_port: Option<u16>,
|
||||||
|
pub age_secs: u64,
|
||||||
|
pub last_accessed_secs: u64,
|
||||||
|
pub last_probed_secs: u64,
|
||||||
|
pub h2_suppressed: bool,
|
||||||
|
pub h3_suppressed: bool,
|
||||||
|
pub h2_cooldown_remaining_secs: Option<u64>,
|
||||||
|
pub h3_cooldown_remaining_secs: Option<u64>,
|
||||||
|
pub h2_consecutive_failures: Option<u32>,
|
||||||
|
pub h3_consecutive_failures: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Exponential backoff: PROTOCOL_FAILURE_COOLDOWN × 2^(n-1), capped at MAX.
|
||||||
|
fn escalate_cooldown(consecutive: u32) -> Duration {
|
||||||
|
let base = PROTOCOL_FAILURE_COOLDOWN.as_secs();
|
||||||
|
let exp = consecutive.saturating_sub(1).min(63) as u64;
|
||||||
|
let secs = base.saturating_mul(1u64.checked_shl(exp as u32).unwrap_or(u64::MAX));
|
||||||
|
Duration::from_secs(secs.min(PROTOCOL_FAILURE_MAX_COOLDOWN.as_secs()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bounded, sliding-TTL protocol detection cache with failure suppression.
|
||||||
///
|
///
|
||||||
/// Memory safety guarantees:
|
/// Memory safety guarantees:
|
||||||
/// - Hard cap at `PROTOCOL_CACHE_MAX_ENTRIES` — cannot grow unboundedly.
|
/// - Hard cap at `PROTOCOL_CACHE_MAX_ENTRIES` — cannot grow unboundedly.
|
||||||
/// - TTL expiry — stale entries naturally age out on lookup.
|
/// - Sliding TTL expiry — entries age out after 1 day without access.
|
||||||
/// - Background cleanup task — proactively removes expired entries every 60s.
|
/// - Background cleanup task — proactively removes expired entries every 60s.
|
||||||
/// - `clear()` — called on route updates to discard stale detections.
|
/// - `clear()` — called on route updates to discard stale detections.
|
||||||
/// - `Drop` — aborts the background task to prevent dangling tokio tasks.
|
/// - `Drop` — aborts the background task to prevent dangling tokio tasks.
|
||||||
pub struct ProtocolCache {
|
pub struct ProtocolCache {
|
||||||
cache: Arc<DashMap<ProtocolCacheKey, CachedEntry>>,
|
cache: Arc<DashMap<ProtocolCacheKey, CachedEntry>>,
|
||||||
|
/// Generic protocol failure suppression map. Tracks per-protocol failure
|
||||||
|
/// records (H2, H3) for each cache key. Used to prevent upgrade signals
|
||||||
|
/// (ALPN, Alt-Svc) from re-introducing failed protocols.
|
||||||
|
failures: Arc<DashMap<ProtocolCacheKey, FailureState>>,
|
||||||
cleanup_handle: Option<tokio::task::JoinHandle<()>>,
|
cleanup_handle: Option<tokio::task::JoinHandle<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,26 +218,40 @@ impl ProtocolCache {
|
|||||||
/// Create a new protocol cache and start the background cleanup task.
|
/// Create a new protocol cache and start the background cleanup task.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let cache: Arc<DashMap<ProtocolCacheKey, CachedEntry>> = Arc::new(DashMap::new());
|
let cache: Arc<DashMap<ProtocolCacheKey, CachedEntry>> = Arc::new(DashMap::new());
|
||||||
|
let failures: Arc<DashMap<ProtocolCacheKey, FailureState>> = Arc::new(DashMap::new());
|
||||||
let cache_clone = Arc::clone(&cache);
|
let cache_clone = Arc::clone(&cache);
|
||||||
|
let failures_clone = Arc::clone(&failures);
|
||||||
let cleanup_handle = tokio::spawn(async move {
|
let cleanup_handle = tokio::spawn(async move {
|
||||||
Self::cleanup_loop(cache_clone).await;
|
Self::cleanup_loop(cache_clone, failures_clone).await;
|
||||||
});
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
cache,
|
cache,
|
||||||
|
failures,
|
||||||
cleanup_handle: Some(cleanup_handle),
|
cleanup_handle: Some(cleanup_handle),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Look up the cached protocol for a backend endpoint.
|
/// Look up the cached protocol for a backend endpoint.
|
||||||
|
///
|
||||||
/// Returns `None` if not cached or expired (caller should probe via ALPN).
|
/// Returns `None` if not cached or expired (caller should probe via ALPN).
|
||||||
|
/// On hit, refreshes `last_accessed_at` (sliding TTL) and sets `needs_reprobe`
|
||||||
|
/// if the entry hasn't been probed in over 5 minutes (H1/H2 only).
|
||||||
pub fn get(&self, key: &ProtocolCacheKey) -> Option<CachedProtocol> {
|
pub fn get(&self, key: &ProtocolCacheKey) -> Option<CachedProtocol> {
|
||||||
let entry = self.cache.get(key)?;
|
let mut entry = self.cache.get_mut(key)?;
|
||||||
if entry.detected_at.elapsed() < PROTOCOL_CACHE_TTL {
|
if entry.last_accessed_at.elapsed() < PROTOCOL_CACHE_TTL {
|
||||||
debug!("Protocol cache hit: {:?} for {}:{} (requested: {:?})", entry.protocol, key.host, key.port, key.requested_host);
|
// Refresh sliding TTL
|
||||||
|
entry.last_accessed_at = Instant::now();
|
||||||
|
|
||||||
|
// H3 is the ceiling — can't ALPN-probe for H3 (discovered via Alt-Svc).
|
||||||
|
// Only H1/H2 entries trigger periodic re-probing.
|
||||||
|
let needs_reprobe = entry.protocol != DetectedProtocol::H3
|
||||||
|
&& entry.last_probed_at.elapsed() >= PROTOCOL_REPROBE_INTERVAL;
|
||||||
|
|
||||||
Some(CachedProtocol {
|
Some(CachedProtocol {
|
||||||
protocol: entry.protocol,
|
protocol: entry.protocol,
|
||||||
h3_port: entry.h3_port,
|
h3_port: entry.h3_port,
|
||||||
|
needs_reprobe,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Expired — remove and return None to trigger re-probe
|
// Expired — remove and return None to trigger re-probe
|
||||||
@@ -105,47 +262,328 @@ impl ProtocolCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Insert a detected protocol into the cache.
|
/// Insert a detected protocol into the cache.
|
||||||
/// If the cache is at capacity, evict the oldest entry first.
|
/// Returns `false` if suppressed due to active failure suppression.
|
||||||
pub fn insert(&self, key: ProtocolCacheKey, protocol: DetectedProtocol) {
|
///
|
||||||
self.insert_with_h3_port(key, protocol, None);
|
/// **Key semantic**: only suppresses if the protocol being inserted matches
|
||||||
|
/// a suppressed protocol. H1 inserts are NEVER suppressed — downgrades
|
||||||
|
/// always succeed.
|
||||||
|
pub fn insert(&self, key: ProtocolCacheKey, protocol: DetectedProtocol, reason: &str) -> bool {
|
||||||
|
if self.is_suppressed(&key, protocol) {
|
||||||
|
debug!(
|
||||||
|
host = %key.host, port = %key.port, domain = ?key.requested_host,
|
||||||
|
protocol = ?protocol,
|
||||||
|
"Protocol cache insert suppressed — recent failure"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.insert_internal(key, protocol, None, reason);
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert an H3 detection result with the Alt-Svc advertised port.
|
/// Insert an H3 detection result with the Alt-Svc advertised port.
|
||||||
pub fn insert_h3(&self, key: ProtocolCacheKey, h3_port: u16) {
|
/// Returns `false` if H3 is suppressed.
|
||||||
self.insert_with_h3_port(key, DetectedProtocol::H3, Some(h3_port));
|
pub fn insert_h3(&self, key: ProtocolCacheKey, h3_port: u16, reason: &str) -> bool {
|
||||||
|
if self.is_suppressed(&key, DetectedProtocol::H3) {
|
||||||
|
debug!(
|
||||||
|
host = %key.host, port = %key.port, domain = ?key.requested_host,
|
||||||
|
"H3 upgrade suppressed — recent failure"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.insert_internal(key, DetectedProtocol::H3, Some(h3_port), reason);
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert a protocol detection result with an optional H3 port.
|
/// Update the cache after an inline ALPN re-probe completes.
|
||||||
fn insert_with_h3_port(&self, key: ProtocolCacheKey, protocol: DetectedProtocol, h3_port: Option<u16>) {
|
///
|
||||||
if self.cache.len() >= PROTOCOL_CACHE_MAX_ENTRIES && !self.cache.contains_key(&key) {
|
/// Always updates `last_probed_at`. If the protocol changed, logs the transition
|
||||||
// Evict the oldest entry to stay within bounds
|
/// and updates the entry. Returns `Some(new_protocol)` if changed, `None` if unchanged.
|
||||||
let oldest = self.cache.iter()
|
pub fn update_probe_result(
|
||||||
.min_by_key(|entry| entry.value().detected_at)
|
&self,
|
||||||
.map(|entry| entry.key().clone());
|
key: &ProtocolCacheKey,
|
||||||
if let Some(oldest_key) = oldest {
|
probed_protocol: DetectedProtocol,
|
||||||
self.cache.remove(&oldest_key);
|
reason: &str,
|
||||||
|
) -> Option<DetectedProtocol> {
|
||||||
|
if let Some(mut entry) = self.cache.get_mut(key) {
|
||||||
|
let old_protocol = entry.protocol;
|
||||||
|
entry.last_probed_at = Instant::now();
|
||||||
|
entry.last_accessed_at = Instant::now();
|
||||||
|
|
||||||
|
if old_protocol != probed_protocol {
|
||||||
|
info!(
|
||||||
|
host = %key.host, port = %key.port, domain = ?key.requested_host,
|
||||||
|
old = %old_protocol, new = %probed_protocol, reason = %reason,
|
||||||
|
"Protocol transition"
|
||||||
|
);
|
||||||
|
entry.protocol = probed_protocol;
|
||||||
|
entry.detected_at = Instant::now();
|
||||||
|
// Clear h3_port if downgrading from H3
|
||||||
|
if old_protocol == DetectedProtocol::H3 && probed_protocol != DetectedProtocol::H3 {
|
||||||
|
entry.h3_port = None;
|
||||||
|
}
|
||||||
|
return Some(probed_protocol);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
host = %key.host, port = %key.port, domain = ?key.requested_host,
|
||||||
|
protocol = %old_protocol, reason = %reason,
|
||||||
|
"Re-probe confirmed — no protocol change"
|
||||||
|
);
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
// Entry was evicted between the get() and the probe completing.
|
||||||
|
// Insert as a fresh entry.
|
||||||
|
self.insert_internal(key.clone(), probed_protocol, None, reason);
|
||||||
|
Some(probed_protocol)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.cache.insert(key, CachedEntry {
|
|
||||||
protocol,
|
/// Record a protocol failure. Future `insert()` calls for this protocol
|
||||||
detected_at: Instant::now(),
|
/// will be suppressed until the escalating cooldown expires.
|
||||||
h3_port,
|
///
|
||||||
|
/// Cooldown escalation: 5s → 10s → 20s → 40s → 80s → 160s → 300s.
|
||||||
|
/// Consecutive counter resets if the previous failure is older than 2× its cooldown.
|
||||||
|
///
|
||||||
|
/// Cascading: when H2 fails, H3 cooldown is reduced to 5s remaining.
|
||||||
|
/// H1 failures are ignored (H1 is the protocol floor).
|
||||||
|
pub fn record_failure(&self, key: ProtocolCacheKey, protocol: DetectedProtocol) {
|
||||||
|
if protocol == DetectedProtocol::H1 {
|
||||||
|
return; // H1 is the floor — nothing to suppress
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut entry = self.failures.entry(key.clone()).or_default();
|
||||||
|
|
||||||
|
let record = entry.get_mut(protocol);
|
||||||
|
let (consecutive, new_cooldown) = match record {
|
||||||
|
Some(existing) if existing.failed_at.elapsed() < existing.cooldown.saturating_mul(2) => {
|
||||||
|
// Still within the "recent" window — escalate
|
||||||
|
let c = existing.consecutive_failures.saturating_add(1)
|
||||||
|
.min(PROTOCOL_FAILURE_ESCALATION_CAP);
|
||||||
|
(c, escalate_cooldown(c))
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// First failure or old failure that expired long ago — reset
|
||||||
|
(1, PROTOCOL_FAILURE_COOLDOWN)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
*record = Some(FailureRecord {
|
||||||
|
failed_at: Instant::now(),
|
||||||
|
cooldown: new_cooldown,
|
||||||
|
consecutive_failures: consecutive,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Cascading: when H2 fails, reduce H3 cooldown to 5s remaining
|
||||||
|
if protocol == DetectedProtocol::H2 {
|
||||||
|
Self::reduce_cooldown_to(entry.h3.as_mut(), PROTOCOL_FAILURE_COOLDOWN);
|
||||||
|
}
|
||||||
|
|
||||||
|
info!(
|
||||||
|
host = %key.host, port = %key.port, domain = ?key.requested_host,
|
||||||
|
protocol = ?protocol,
|
||||||
|
consecutive = consecutive,
|
||||||
|
cooldown_secs = new_cooldown.as_secs(),
|
||||||
|
"Protocol failure recorded — suppressing for {:?}", new_cooldown
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether a protocol is currently suppressed for the given key.
|
||||||
|
/// Returns `true` if the protocol failed within its cooldown period.
|
||||||
|
/// H1 is never suppressed.
|
||||||
|
pub fn is_suppressed(&self, key: &ProtocolCacheKey, protocol: DetectedProtocol) -> bool {
|
||||||
|
if protocol == DetectedProtocol::H1 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.failures.get(key)
|
||||||
|
.and_then(|entry| entry.get(protocol).map(|r| r.failed_at.elapsed() < r.cooldown))
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check whether a protocol can be retried (for within-request escalation).
|
||||||
|
/// Returns `true` if there's no failure record OR if ≥5s have passed since
|
||||||
|
/// the last attempt. More permissive than `is_suppressed`.
|
||||||
|
pub fn can_retry(&self, key: &ProtocolCacheKey, protocol: DetectedProtocol) -> bool {
|
||||||
|
if protocol == DetectedProtocol::H1 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
match self.failures.get(key) {
|
||||||
|
Some(entry) => match entry.get(protocol) {
|
||||||
|
Some(r) => r.failed_at.elapsed() >= PROTOCOL_FAILURE_COOLDOWN,
|
||||||
|
None => true, // no failure record
|
||||||
|
},
|
||||||
|
None => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Record a retry attempt WITHOUT escalating the cooldown.
|
||||||
|
/// Resets the `failed_at` timestamp to prevent rapid retries (5s gate).
|
||||||
|
/// Called before an escalation attempt. If the attempt fails,
|
||||||
|
/// `record_failure` should be called afterward with proper escalation.
|
||||||
|
pub fn record_retry_attempt(&self, key: &ProtocolCacheKey, protocol: DetectedProtocol) {
|
||||||
|
if protocol == DetectedProtocol::H1 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(mut entry) = self.failures.get_mut(key) {
|
||||||
|
if let Some(ref mut r) = entry.get_mut(protocol) {
|
||||||
|
r.failed_at = Instant::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear the failure record for a protocol (it recovered).
|
||||||
|
/// Called when an escalation retry succeeds.
|
||||||
|
pub fn clear_failure(&self, key: &ProtocolCacheKey, protocol: DetectedProtocol) {
|
||||||
|
if protocol == DetectedProtocol::H1 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(mut entry) = self.failures.get_mut(key) {
|
||||||
|
*entry.get_mut(protocol) = None;
|
||||||
|
if entry.is_empty() {
|
||||||
|
drop(entry);
|
||||||
|
self.failures.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evict a cache entry entirely. Called when all protocol probes (H3, H2, H1)
|
||||||
|
/// have failed for a backend.
|
||||||
|
pub fn evict(&self, key: &ProtocolCacheKey) {
|
||||||
|
self.cache.remove(key);
|
||||||
|
self.failures.remove(key);
|
||||||
|
info!(
|
||||||
|
host = %key.host, port = %key.port, domain = ?key.requested_host,
|
||||||
|
"Cache entry evicted — all protocols failed"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear all entries. Called on route updates to discard stale detections.
|
/// Clear all entries. Called on route updates to discard stale detections.
|
||||||
pub fn clear(&self) {
|
pub fn clear(&self) {
|
||||||
self.cache.clear();
|
self.cache.clear();
|
||||||
|
self.failures.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Background cleanup loop — removes expired entries every `PROTOCOL_CACHE_CLEANUP_INTERVAL`.
|
/// Snapshot all non-expired cache entries for metrics/UI display.
|
||||||
async fn cleanup_loop(cache: Arc<DashMap<ProtocolCacheKey, CachedEntry>>) {
|
pub fn snapshot(&self) -> Vec<ProtocolCacheEntry> {
|
||||||
|
self.cache.iter()
|
||||||
|
.filter(|entry| entry.value().last_accessed_at.elapsed() < PROTOCOL_CACHE_TTL)
|
||||||
|
.map(|entry| {
|
||||||
|
let key = entry.key();
|
||||||
|
let val = entry.value();
|
||||||
|
let failure_info = self.failures.get(key);
|
||||||
|
|
||||||
|
let (h2_sup, h2_cd, h2_cons) = Self::suppression_info(
|
||||||
|
failure_info.as_deref().and_then(|f| f.h2.as_ref()),
|
||||||
|
);
|
||||||
|
let (h3_sup, h3_cd, h3_cons) = Self::suppression_info(
|
||||||
|
failure_info.as_deref().and_then(|f| f.h3.as_ref()),
|
||||||
|
);
|
||||||
|
|
||||||
|
ProtocolCacheEntry {
|
||||||
|
host: key.host.clone(),
|
||||||
|
port: key.port,
|
||||||
|
domain: key.requested_host.clone(),
|
||||||
|
protocol: match val.protocol {
|
||||||
|
DetectedProtocol::H1 => "h1".to_string(),
|
||||||
|
DetectedProtocol::H2 => "h2".to_string(),
|
||||||
|
DetectedProtocol::H3 => "h3".to_string(),
|
||||||
|
},
|
||||||
|
h3_port: val.h3_port,
|
||||||
|
age_secs: val.detected_at.elapsed().as_secs(),
|
||||||
|
last_accessed_secs: val.last_accessed_at.elapsed().as_secs(),
|
||||||
|
last_probed_secs: val.last_probed_at.elapsed().as_secs(),
|
||||||
|
h2_suppressed: h2_sup,
|
||||||
|
h3_suppressed: h3_sup,
|
||||||
|
h2_cooldown_remaining_secs: h2_cd,
|
||||||
|
h3_cooldown_remaining_secs: h3_cd,
|
||||||
|
h2_consecutive_failures: h2_cons,
|
||||||
|
h3_consecutive_failures: h3_cons,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Internal helpers ---
|
||||||
|
|
||||||
|
/// Insert a protocol detection result with an optional H3 port.
|
||||||
|
/// Logs protocol transitions when overwriting an existing entry.
|
||||||
|
/// No suppression check — callers must check before calling.
|
||||||
|
fn insert_internal(&self, key: ProtocolCacheKey, protocol: DetectedProtocol, h3_port: Option<u16>, reason: &str) {
|
||||||
|
// Check for existing entry to log protocol transitions
|
||||||
|
if let Some(existing) = self.cache.get(&key) {
|
||||||
|
if existing.protocol != protocol {
|
||||||
|
info!(
|
||||||
|
host = %key.host, port = %key.port, domain = ?key.requested_host,
|
||||||
|
old = %existing.protocol, new = %protocol, reason = %reason,
|
||||||
|
"Protocol transition"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
drop(existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evict oldest entry if at capacity
|
||||||
|
if self.cache.len() >= PROTOCOL_CACHE_MAX_ENTRIES && !self.cache.contains_key(&key) {
|
||||||
|
let oldest = self.cache.iter()
|
||||||
|
.min_by_key(|entry| entry.value().last_accessed_at)
|
||||||
|
.map(|entry| entry.key().clone());
|
||||||
|
if let Some(oldest_key) = oldest {
|
||||||
|
self.cache.remove(&oldest_key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let now = Instant::now();
|
||||||
|
self.cache.insert(key, CachedEntry {
|
||||||
|
protocol,
|
||||||
|
detected_at: now,
|
||||||
|
last_accessed_at: now,
|
||||||
|
last_probed_at: now,
|
||||||
|
h3_port,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reduce a failure record's remaining cooldown to `target`, if it currently
|
||||||
|
/// has MORE than `target` remaining. Never increases cooldown.
|
||||||
|
fn reduce_cooldown_to(record: Option<&mut FailureRecord>, target: Duration) {
|
||||||
|
if let Some(r) = record {
|
||||||
|
let elapsed = r.failed_at.elapsed();
|
||||||
|
if elapsed < r.cooldown {
|
||||||
|
let remaining = r.cooldown - elapsed;
|
||||||
|
if remaining > target {
|
||||||
|
// Shrink cooldown so it expires in `target` from now
|
||||||
|
r.cooldown = elapsed + target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract suppression info from a failure record for metrics.
|
||||||
|
fn suppression_info(record: Option<&FailureRecord>) -> (bool, Option<u64>, Option<u32>) {
|
||||||
|
match record {
|
||||||
|
Some(r) => {
|
||||||
|
let elapsed = r.failed_at.elapsed();
|
||||||
|
let suppressed = elapsed < r.cooldown;
|
||||||
|
let remaining = if suppressed {
|
||||||
|
Some((r.cooldown - elapsed).as_secs())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
(suppressed, remaining, Some(r.consecutive_failures))
|
||||||
|
}
|
||||||
|
None => (false, None, None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Background cleanup loop.
|
||||||
|
async fn cleanup_loop(
|
||||||
|
cache: Arc<DashMap<ProtocolCacheKey, CachedEntry>>,
|
||||||
|
failures: Arc<DashMap<ProtocolCacheKey, FailureState>>,
|
||||||
|
) {
|
||||||
let mut interval = tokio::time::interval(PROTOCOL_CACHE_CLEANUP_INTERVAL);
|
let mut interval = tokio::time::interval(PROTOCOL_CACHE_CLEANUP_INTERVAL);
|
||||||
loop {
|
loop {
|
||||||
interval.tick().await;
|
interval.tick().await;
|
||||||
|
|
||||||
|
// Clean expired cache entries (sliding TTL based on last_accessed_at)
|
||||||
let expired: Vec<ProtocolCacheKey> = cache.iter()
|
let expired: Vec<ProtocolCacheKey> = cache.iter()
|
||||||
.filter(|entry| entry.value().detected_at.elapsed() >= PROTOCOL_CACHE_TTL)
|
.filter(|entry| entry.value().last_accessed_at.elapsed() >= PROTOCOL_CACHE_TTL)
|
||||||
.map(|entry| entry.key().clone())
|
.map(|entry| entry.key().clone())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -155,6 +593,31 @@ impl ProtocolCache {
|
|||||||
cache.remove(&key);
|
cache.remove(&key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean fully-expired failure entries
|
||||||
|
let expired_failures: Vec<ProtocolCacheKey> = failures.iter()
|
||||||
|
.filter(|entry| entry.value().all_expired())
|
||||||
|
.map(|entry| entry.key().clone())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if !expired_failures.is_empty() {
|
||||||
|
debug!("Protocol cache cleanup: removing {} expired failure entries", expired_failures.len());
|
||||||
|
for key in expired_failures {
|
||||||
|
failures.remove(&key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safety net: cap failures map at 2× max entries
|
||||||
|
if failures.len() > PROTOCOL_CACHE_MAX_ENTRIES * 2 {
|
||||||
|
let oldest: Vec<ProtocolCacheKey> = failures.iter()
|
||||||
|
.filter(|e| e.value().all_expired())
|
||||||
|
.map(|e| e.key().clone())
|
||||||
|
.take(failures.len() - PROTOCOL_CACHE_MAX_ENTRIES)
|
||||||
|
.collect();
|
||||||
|
for key in oldest {
|
||||||
|
failures.remove(&key);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ pub struct ConnActivity {
|
|||||||
/// checks the backend's original response headers for Alt-Svc before our
|
/// checks the backend's original response headers for Alt-Svc before our
|
||||||
/// ResponseFilter injects its own. None when not in auto-detect mode or after H3 failure.
|
/// ResponseFilter injects its own. None when not in auto-detect mode or after H3 failure.
|
||||||
alt_svc_cache_key: Option<crate::protocol_cache::ProtocolCacheKey>,
|
alt_svc_cache_key: Option<crate::protocol_cache::ProtocolCacheKey>,
|
||||||
|
/// The upstream request path that triggered Alt-Svc discovery. Logged for traceability.
|
||||||
|
alt_svc_request_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConnActivity {
|
impl ConnActivity {
|
||||||
@@ -58,6 +60,7 @@ impl ConnActivity {
|
|||||||
start: std::time::Instant::now(),
|
start: std::time::Instant::now(),
|
||||||
active_requests: None,
|
active_requests: None,
|
||||||
alt_svc_cache_key: None,
|
alt_svc_cache_key: None,
|
||||||
|
alt_svc_request_url: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,6 +314,11 @@ impl HttpProxyService {
|
|||||||
self.protocol_cache.clear();
|
self.protocol_cache.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Snapshot the protocol cache for metrics/UI display.
|
||||||
|
pub fn protocol_cache_snapshot(&self) -> Vec<crate::protocol_cache::ProtocolCacheEntry> {
|
||||||
|
self.protocol_cache.snapshot()
|
||||||
|
}
|
||||||
|
|
||||||
/// Handle an incoming HTTP connection on a plain TCP stream.
|
/// Handle an incoming HTTP connection on a plain TCP stream.
|
||||||
pub async fn handle_connection(
|
pub async fn handle_connection(
|
||||||
self: Arc<Self>,
|
self: Arc<Self>,
|
||||||
@@ -366,7 +374,7 @@ impl HttpProxyService {
|
|||||||
let cn = cancel_inner.clone();
|
let cn = cancel_inner.clone();
|
||||||
let la = Arc::clone(&la_inner);
|
let la = Arc::clone(&la_inner);
|
||||||
let st = start;
|
let st = start;
|
||||||
let ca = ConnActivity { last_activity: Arc::clone(&la_inner), start, active_requests: Some(Arc::clone(&ar_inner)), alt_svc_cache_key: None };
|
let ca = ConnActivity { last_activity: Arc::clone(&la_inner), start, active_requests: Some(Arc::clone(&ar_inner)), alt_svc_cache_key: None, alt_svc_request_url: None };
|
||||||
async move {
|
async move {
|
||||||
let req = req.map(|body| BoxBody::new(body));
|
let req = req.map(|body| BoxBody::new(body));
|
||||||
let result = svc.handle_request(req, peer, port, cn, ca).await;
|
let result = svc.handle_request(req, peer, port, cn, ca).await;
|
||||||
@@ -701,6 +709,14 @@ impl HttpProxyService {
|
|||||||
port: upstream.port,
|
port: upstream.port,
|
||||||
requested_host: host.clone(),
|
requested_host: host.clone(),
|
||||||
};
|
};
|
||||||
|
// Save cached H3 port for within-request escalation (may be needed later
|
||||||
|
// if TCP connect fails and we escalate to H3 as a last resort)
|
||||||
|
let cached_h3_port = self.protocol_cache.get(&protocol_cache_key)
|
||||||
|
.and_then(|c| c.h3_port);
|
||||||
|
|
||||||
|
// Track whether this ALPN probe is a periodic re-probe (vs first-time detection)
|
||||||
|
let mut is_reprobe = false;
|
||||||
|
|
||||||
let protocol_decision = match backend_protocol_mode {
|
let protocol_decision = match backend_protocol_mode {
|
||||||
rustproxy_config::BackendProtocol::Http1 => ProtocolDecision::H1,
|
rustproxy_config::BackendProtocol::Http1 => ProtocolDecision::H1,
|
||||||
rustproxy_config::BackendProtocol::Http2 => ProtocolDecision::H2,
|
rustproxy_config::BackendProtocol::Http2 => ProtocolDecision::H2,
|
||||||
@@ -711,19 +727,40 @@ impl HttpProxyService {
|
|||||||
ProtocolDecision::H1
|
ProtocolDecision::H1
|
||||||
} else {
|
} else {
|
||||||
match self.protocol_cache.get(&protocol_cache_key) {
|
match self.protocol_cache.get(&protocol_cache_key) {
|
||||||
|
Some(cached) if cached.needs_reprobe => {
|
||||||
|
// Entry exists but 5+ minutes since last probe — force ALPN re-probe
|
||||||
|
// (only fires for H1/H2; H3 entries have needs_reprobe=false)
|
||||||
|
is_reprobe = true;
|
||||||
|
ProtocolDecision::AlpnProbe
|
||||||
|
}
|
||||||
Some(cached) => match cached.protocol {
|
Some(cached) => match cached.protocol {
|
||||||
crate::protocol_cache::DetectedProtocol::H3 => {
|
crate::protocol_cache::DetectedProtocol::H3 => {
|
||||||
if let Some(h3_port) = cached.h3_port {
|
if self.protocol_cache.is_suppressed(&protocol_cache_key, crate::protocol_cache::DetectedProtocol::H3) {
|
||||||
|
// H3 cached but suppressed — fall back to ALPN probe
|
||||||
|
ProtocolDecision::AlpnProbe
|
||||||
|
} else if let Some(h3_port) = cached.h3_port {
|
||||||
ProtocolDecision::H3 { port: h3_port }
|
ProtocolDecision::H3 { port: h3_port }
|
||||||
} else {
|
} else {
|
||||||
// H3 cached but no port — fall back to ALPN probe
|
|
||||||
ProtocolDecision::AlpnProbe
|
ProtocolDecision::AlpnProbe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
crate::protocol_cache::DetectedProtocol::H2 => ProtocolDecision::H2,
|
crate::protocol_cache::DetectedProtocol::H2 => {
|
||||||
|
if self.protocol_cache.is_suppressed(&protocol_cache_key, crate::protocol_cache::DetectedProtocol::H2) {
|
||||||
|
ProtocolDecision::H1
|
||||||
|
} else {
|
||||||
|
ProtocolDecision::H2
|
||||||
|
}
|
||||||
|
}
|
||||||
crate::protocol_cache::DetectedProtocol::H1 => ProtocolDecision::H1,
|
crate::protocol_cache::DetectedProtocol::H1 => ProtocolDecision::H1,
|
||||||
},
|
},
|
||||||
None => ProtocolDecision::AlpnProbe,
|
None => {
|
||||||
|
// Cache miss — skip ALPN probe if H2 is suppressed
|
||||||
|
if self.protocol_cache.is_suppressed(&protocol_cache_key, crate::protocol_cache::DetectedProtocol::H2) {
|
||||||
|
ProtocolDecision::H1
|
||||||
|
} else {
|
||||||
|
ProtocolDecision::AlpnProbe
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -741,6 +778,7 @@ impl HttpProxyService {
|
|||||||
// the backend's original Alt-Svc header before ResponseFilter injects our own.
|
// the backend's original Alt-Svc header before ResponseFilter injects our own.
|
||||||
if is_auto_detect_mode {
|
if is_auto_detect_mode {
|
||||||
conn_activity.alt_svc_cache_key = Some(protocol_cache_key.clone());
|
conn_activity.alt_svc_cache_key = Some(protocol_cache_key.clone());
|
||||||
|
conn_activity.alt_svc_request_url = Some(upstream_path.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- H3 path: try QUIC connection before TCP ---
|
// --- H3 path: try QUIC connection before TCP ---
|
||||||
@@ -776,8 +814,16 @@ impl HttpProxyService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!(backend = %upstream_key, error = %e,
|
warn!(backend = %upstream_key, domain = %domain_str, error = %e,
|
||||||
"H3 backend connect failed, falling back to H2/H1");
|
"H3 backend connect failed, falling back to H2/H1");
|
||||||
|
// Record failure with escalating cooldown — prevents Alt-Svc
|
||||||
|
// from re-upgrading to H3 during cooldown period
|
||||||
|
if is_auto_detect_mode {
|
||||||
|
self.protocol_cache.record_failure(
|
||||||
|
protocol_cache_key.clone(),
|
||||||
|
crate::protocol_cache::DetectedProtocol::H3,
|
||||||
|
);
|
||||||
|
}
|
||||||
// Suppress Alt-Svc caching for the fallback to prevent re-caching H3
|
// Suppress Alt-Svc caching for the fallback to prevent re-caching H3
|
||||||
// from our own injected Alt-Svc header or a stale backend Alt-Svc
|
// from our own injected Alt-Svc header or a stale backend Alt-Svc
|
||||||
conn_activity.alt_svc_cache_key = None;
|
conn_activity.alt_svc_cache_key = None;
|
||||||
@@ -860,7 +906,7 @@ impl HttpProxyService {
|
|||||||
let alpn = tls.get_ref().1.alpn_protocol();
|
let alpn = tls.get_ref().1.alpn_protocol();
|
||||||
let is_h2 = alpn.map(|p| p == b"h2").unwrap_or(false);
|
let is_h2 = alpn.map(|p| p == b"h2").unwrap_or(false);
|
||||||
|
|
||||||
// Cache the result
|
// Cache the result (or update existing entry for re-probes)
|
||||||
let cache_key = crate::protocol_cache::ProtocolCacheKey {
|
let cache_key = crate::protocol_cache::ProtocolCacheKey {
|
||||||
host: upstream.host.clone(),
|
host: upstream.host.clone(),
|
||||||
port: upstream.port,
|
port: upstream.port,
|
||||||
@@ -871,13 +917,18 @@ impl HttpProxyService {
|
|||||||
} else {
|
} else {
|
||||||
crate::protocol_cache::DetectedProtocol::H1
|
crate::protocol_cache::DetectedProtocol::H1
|
||||||
};
|
};
|
||||||
self.protocol_cache.insert(cache_key, detected);
|
if is_reprobe {
|
||||||
|
self.protocol_cache.update_probe_result(&cache_key, detected, "periodic ALPN re-probe");
|
||||||
|
} else {
|
||||||
|
self.protocol_cache.insert(cache_key, detected, "initial ALPN detection");
|
||||||
|
}
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
backend = %upstream_key,
|
backend = %upstream_key,
|
||||||
domain = %domain_str,
|
domain = %domain_str,
|
||||||
protocol = if is_h2 { "h2" } else { "h1" },
|
protocol = if is_h2 { "h2" } else { "h1" },
|
||||||
connect_time_ms = %connect_start.elapsed().as_millis(),
|
connect_time_ms = %connect_start.elapsed().as_millis(),
|
||||||
|
reprobe = is_reprobe,
|
||||||
"Backend protocol detected via ALPN"
|
"Backend protocol detected via ALPN"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -899,6 +950,38 @@ impl HttpProxyService {
|
|||||||
);
|
);
|
||||||
self.metrics.backend_connect_error(&upstream_key);
|
self.metrics.backend_connect_error(&upstream_key);
|
||||||
self.upstream_selector.connection_ended(&upstream_key);
|
self.upstream_selector.connection_ended(&upstream_key);
|
||||||
|
|
||||||
|
// --- Within-request escalation: try H3 via QUIC if retryable ---
|
||||||
|
if is_auto_detect_mode {
|
||||||
|
if let Some(h3_port) = cached_h3_port {
|
||||||
|
if self.protocol_cache.can_retry(&protocol_cache_key, crate::protocol_cache::DetectedProtocol::H3) {
|
||||||
|
self.protocol_cache.record_retry_attempt(&protocol_cache_key, crate::protocol_cache::DetectedProtocol::H3);
|
||||||
|
debug!(backend = %upstream_key, domain = %domain_str, "TLS connect failed — escalating to H3");
|
||||||
|
match self.connect_quic_backend(&upstream.host, h3_port).await {
|
||||||
|
Ok(quic_conn) => {
|
||||||
|
self.protocol_cache.clear_failure(&protocol_cache_key, crate::protocol_cache::DetectedProtocol::H3);
|
||||||
|
self.protocol_cache.insert_h3(protocol_cache_key.clone(), h3_port, "recovery — TLS failed, H3 succeeded");
|
||||||
|
let h3_pool_key = crate::connection_pool::PoolKey {
|
||||||
|
host: upstream.host.clone(), port: h3_port, use_tls: true,
|
||||||
|
protocol: crate::connection_pool::PoolProtocol::H3,
|
||||||
|
};
|
||||||
|
let result = self.forward_h3(
|
||||||
|
quic_conn, parts, body, upstream_headers, &upstream_path,
|
||||||
|
route_match.route, route_id, &ip_str, &h3_pool_key, domain_str, &conn_activity, &upstream_key,
|
||||||
|
).await;
|
||||||
|
self.upstream_selector.connection_ended(&upstream_key);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
Err(e3) => {
|
||||||
|
debug!(backend = %upstream_key, error = %e3, "H3 escalation also failed");
|
||||||
|
self.protocol_cache.record_failure(protocol_cache_key.clone(), crate::protocol_cache::DetectedProtocol::H3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// All protocols failed — evict cache entry
|
||||||
|
self.protocol_cache.evict(&protocol_cache_key);
|
||||||
|
}
|
||||||
return Ok(error_response(StatusCode::BAD_GATEWAY, "Backend TLS unavailable"));
|
return Ok(error_response(StatusCode::BAD_GATEWAY, "Backend TLS unavailable"));
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
@@ -910,6 +993,38 @@ impl HttpProxyService {
|
|||||||
);
|
);
|
||||||
self.metrics.backend_connect_error(&upstream_key);
|
self.metrics.backend_connect_error(&upstream_key);
|
||||||
self.upstream_selector.connection_ended(&upstream_key);
|
self.upstream_selector.connection_ended(&upstream_key);
|
||||||
|
|
||||||
|
// --- Within-request escalation: try H3 via QUIC if retryable ---
|
||||||
|
if is_auto_detect_mode {
|
||||||
|
if let Some(h3_port) = cached_h3_port {
|
||||||
|
if self.protocol_cache.can_retry(&protocol_cache_key, crate::protocol_cache::DetectedProtocol::H3) {
|
||||||
|
self.protocol_cache.record_retry_attempt(&protocol_cache_key, crate::protocol_cache::DetectedProtocol::H3);
|
||||||
|
debug!(backend = %upstream_key, domain = %domain_str, "TLS connect timeout — escalating to H3");
|
||||||
|
match self.connect_quic_backend(&upstream.host, h3_port).await {
|
||||||
|
Ok(quic_conn) => {
|
||||||
|
self.protocol_cache.clear_failure(&protocol_cache_key, crate::protocol_cache::DetectedProtocol::H3);
|
||||||
|
self.protocol_cache.insert_h3(protocol_cache_key.clone(), h3_port, "recovery — TLS timeout, H3 succeeded");
|
||||||
|
let h3_pool_key = crate::connection_pool::PoolKey {
|
||||||
|
host: upstream.host.clone(), port: h3_port, use_tls: true,
|
||||||
|
protocol: crate::connection_pool::PoolProtocol::H3,
|
||||||
|
};
|
||||||
|
let result = self.forward_h3(
|
||||||
|
quic_conn, parts, body, upstream_headers, &upstream_path,
|
||||||
|
route_match.route, route_id, &ip_str, &h3_pool_key, domain_str, &conn_activity, &upstream_key,
|
||||||
|
).await;
|
||||||
|
self.upstream_selector.connection_ended(&upstream_key);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
Err(e3) => {
|
||||||
|
debug!(backend = %upstream_key, error = %e3, "H3 escalation also failed");
|
||||||
|
self.protocol_cache.record_failure(protocol_cache_key.clone(), crate::protocol_cache::DetectedProtocol::H3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// All protocols failed — evict cache entry
|
||||||
|
self.protocol_cache.evict(&protocol_cache_key);
|
||||||
|
}
|
||||||
return Ok(error_response(StatusCode::GATEWAY_TIMEOUT, "Backend TLS connect timeout"));
|
return Ok(error_response(StatusCode::GATEWAY_TIMEOUT, "Backend TLS connect timeout"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -937,6 +1052,38 @@ impl HttpProxyService {
|
|||||||
);
|
);
|
||||||
self.metrics.backend_connect_error(&upstream_key);
|
self.metrics.backend_connect_error(&upstream_key);
|
||||||
self.upstream_selector.connection_ended(&upstream_key);
|
self.upstream_selector.connection_ended(&upstream_key);
|
||||||
|
|
||||||
|
// --- Within-request escalation: try H3 via QUIC if retryable ---
|
||||||
|
if is_auto_detect_mode {
|
||||||
|
if let Some(h3_port) = cached_h3_port {
|
||||||
|
if self.protocol_cache.can_retry(&protocol_cache_key, crate::protocol_cache::DetectedProtocol::H3) {
|
||||||
|
self.protocol_cache.record_retry_attempt(&protocol_cache_key, crate::protocol_cache::DetectedProtocol::H3);
|
||||||
|
debug!(backend = %upstream_key, domain = %domain_str, "TCP connect failed — escalating to H3");
|
||||||
|
match self.connect_quic_backend(&upstream.host, h3_port).await {
|
||||||
|
Ok(quic_conn) => {
|
||||||
|
self.protocol_cache.clear_failure(&protocol_cache_key, crate::protocol_cache::DetectedProtocol::H3);
|
||||||
|
self.protocol_cache.insert_h3(protocol_cache_key.clone(), h3_port, "recovery — TCP failed, H3 succeeded");
|
||||||
|
let h3_pool_key = crate::connection_pool::PoolKey {
|
||||||
|
host: upstream.host.clone(), port: h3_port, use_tls: true,
|
||||||
|
protocol: crate::connection_pool::PoolProtocol::H3,
|
||||||
|
};
|
||||||
|
let result = self.forward_h3(
|
||||||
|
quic_conn, parts, body, upstream_headers, &upstream_path,
|
||||||
|
route_match.route, route_id, &ip_str, &h3_pool_key, domain_str, &conn_activity, &upstream_key,
|
||||||
|
).await;
|
||||||
|
self.upstream_selector.connection_ended(&upstream_key);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
Err(e3) => {
|
||||||
|
debug!(backend = %upstream_key, error = %e3, "H3 escalation also failed");
|
||||||
|
self.protocol_cache.record_failure(protocol_cache_key.clone(), crate::protocol_cache::DetectedProtocol::H3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// All protocols failed — evict cache entry
|
||||||
|
self.protocol_cache.evict(&protocol_cache_key);
|
||||||
|
}
|
||||||
return Ok(error_response(StatusCode::BAD_GATEWAY, "Backend unavailable"));
|
return Ok(error_response(StatusCode::BAD_GATEWAY, "Backend unavailable"));
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
@@ -948,6 +1095,38 @@ impl HttpProxyService {
|
|||||||
);
|
);
|
||||||
self.metrics.backend_connect_error(&upstream_key);
|
self.metrics.backend_connect_error(&upstream_key);
|
||||||
self.upstream_selector.connection_ended(&upstream_key);
|
self.upstream_selector.connection_ended(&upstream_key);
|
||||||
|
|
||||||
|
// --- Within-request escalation: try H3 via QUIC if retryable ---
|
||||||
|
if is_auto_detect_mode {
|
||||||
|
if let Some(h3_port) = cached_h3_port {
|
||||||
|
if self.protocol_cache.can_retry(&protocol_cache_key, crate::protocol_cache::DetectedProtocol::H3) {
|
||||||
|
self.protocol_cache.record_retry_attempt(&protocol_cache_key, crate::protocol_cache::DetectedProtocol::H3);
|
||||||
|
debug!(backend = %upstream_key, domain = %domain_str, "TCP connect timeout — escalating to H3");
|
||||||
|
match self.connect_quic_backend(&upstream.host, h3_port).await {
|
||||||
|
Ok(quic_conn) => {
|
||||||
|
self.protocol_cache.clear_failure(&protocol_cache_key, crate::protocol_cache::DetectedProtocol::H3);
|
||||||
|
self.protocol_cache.insert_h3(protocol_cache_key.clone(), h3_port, "recovery — TCP timeout, H3 succeeded");
|
||||||
|
let h3_pool_key = crate::connection_pool::PoolKey {
|
||||||
|
host: upstream.host.clone(), port: h3_port, use_tls: true,
|
||||||
|
protocol: crate::connection_pool::PoolProtocol::H3,
|
||||||
|
};
|
||||||
|
let result = self.forward_h3(
|
||||||
|
quic_conn, parts, body, upstream_headers, &upstream_path,
|
||||||
|
route_match.route, route_id, &ip_str, &h3_pool_key, domain_str, &conn_activity, &upstream_key,
|
||||||
|
).await;
|
||||||
|
self.upstream_selector.connection_ended(&upstream_key);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
Err(e3) => {
|
||||||
|
debug!(backend = %upstream_key, error = %e3, "H3 escalation also failed");
|
||||||
|
self.protocol_cache.record_failure(protocol_cache_key.clone(), crate::protocol_cache::DetectedProtocol::H3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// All protocols failed — evict cache entry
|
||||||
|
self.protocol_cache.evict(&protocol_cache_key);
|
||||||
|
}
|
||||||
return Ok(error_response(StatusCode::GATEWAY_TIMEOUT, "Backend connect timeout"));
|
return Ok(error_response(StatusCode::GATEWAY_TIMEOUT, "Backend connect timeout"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1030,8 +1209,10 @@ impl HttpProxyService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(e) = conn.await {
|
match tokio::time::timeout(std::time::Duration::from_secs(300), conn).await {
|
||||||
debug!("Upstream connection error: {}", e);
|
Ok(Err(e)) => debug!("Upstream connection error: {}", e),
|
||||||
|
Err(_) => debug!("H1 connection driver timed out after 300s"),
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1416,7 +1597,12 @@ impl HttpProxyService {
|
|||||||
port: upstream.port,
|
port: upstream.port,
|
||||||
requested_host: requested_host.clone(),
|
requested_host: requested_host.clone(),
|
||||||
};
|
};
|
||||||
self.protocol_cache.insert(cache_key, crate::protocol_cache::DetectedProtocol::H1);
|
// Record H2 failure (escalating cooldown) before downgrading cache to H1
|
||||||
|
self.protocol_cache.record_failure(
|
||||||
|
cache_key.clone(),
|
||||||
|
crate::protocol_cache::DetectedProtocol::H2,
|
||||||
|
);
|
||||||
|
self.protocol_cache.insert(cache_key.clone(), crate::protocol_cache::DetectedProtocol::H1, "H2 handshake timeout — downgrade");
|
||||||
|
|
||||||
match self.reconnect_backend(upstream, domain, backend_key).await {
|
match self.reconnect_backend(upstream, domain, backend_key).await {
|
||||||
Some(fallback_backend) => {
|
Some(fallback_backend) => {
|
||||||
@@ -1435,6 +1621,8 @@ impl HttpProxyService {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
// H2 failed and H1 reconnect also failed — evict cache
|
||||||
|
self.protocol_cache.evict(&cache_key);
|
||||||
Ok(error_response(StatusCode::BAD_GATEWAY, "Backend unavailable after H2 timeout fallback"))
|
Ok(error_response(StatusCode::BAD_GATEWAY, "Backend unavailable after H2 timeout fallback"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1549,13 +1737,17 @@ impl HttpProxyService {
|
|||||||
self.metrics.backend_h2_failure(backend_key);
|
self.metrics.backend_h2_failure(backend_key);
|
||||||
self.metrics.backend_handshake_error(backend_key);
|
self.metrics.backend_handshake_error(backend_key);
|
||||||
|
|
||||||
// Update cache to H1 so subsequent requests skip H2
|
// Record H2 failure (escalating cooldown) and downgrade cache to H1
|
||||||
let cache_key = crate::protocol_cache::ProtocolCacheKey {
|
let cache_key = crate::protocol_cache::ProtocolCacheKey {
|
||||||
host: upstream.host.clone(),
|
host: upstream.host.clone(),
|
||||||
port: upstream.port,
|
port: upstream.port,
|
||||||
requested_host: requested_host.clone(),
|
requested_host: requested_host.clone(),
|
||||||
};
|
};
|
||||||
self.protocol_cache.insert(cache_key, crate::protocol_cache::DetectedProtocol::H1);
|
self.protocol_cache.record_failure(
|
||||||
|
cache_key.clone(),
|
||||||
|
crate::protocol_cache::DetectedProtocol::H2,
|
||||||
|
);
|
||||||
|
self.protocol_cache.insert(cache_key.clone(), crate::protocol_cache::DetectedProtocol::H1, "H2 handshake error — downgrade");
|
||||||
|
|
||||||
// Reconnect for H1 (the original io was consumed by the failed h2 handshake)
|
// Reconnect for H1 (the original io was consumed by the failed h2 handshake)
|
||||||
match self.reconnect_backend(upstream, domain, backend_key).await {
|
match self.reconnect_backend(upstream, domain, backend_key).await {
|
||||||
@@ -1576,6 +1768,8 @@ impl HttpProxyService {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
// H2 failed and H1 reconnect also failed — evict cache
|
||||||
|
self.protocol_cache.evict(&cache_key);
|
||||||
Ok(error_response(StatusCode::BAD_GATEWAY, "Backend unavailable after H2 fallback"))
|
Ok(error_response(StatusCode::BAD_GATEWAY, "Backend unavailable after H2 fallback"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1611,8 +1805,10 @@ impl HttpProxyService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(e) = conn.await {
|
match tokio::time::timeout(std::time::Duration::from_secs(300), conn).await {
|
||||||
debug!("H1 fallback: upstream connection error: {}", e);
|
Ok(Err(e)) => debug!("H1 fallback: upstream connection error: {}", e),
|
||||||
|
Err(_) => debug!("H1 fallback: connection driver timed out after 300s"),
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1791,8 +1987,10 @@ impl HttpProxyService {
|
|||||||
if let Some(ref cache_key) = conn_activity.alt_svc_cache_key {
|
if let Some(ref cache_key) = conn_activity.alt_svc_cache_key {
|
||||||
if let Some(alt_svc) = resp_parts.headers.get("alt-svc").and_then(|v| v.to_str().ok()) {
|
if let Some(alt_svc) = resp_parts.headers.get("alt-svc").and_then(|v| v.to_str().ok()) {
|
||||||
if let Some(h3_port) = parse_alt_svc_h3_port(alt_svc) {
|
if let Some(h3_port) = parse_alt_svc_h3_port(alt_svc) {
|
||||||
debug!(h3_port, "Backend advertises H3 via Alt-Svc");
|
let url = conn_activity.alt_svc_request_url.as_deref().unwrap_or("-");
|
||||||
self.protocol_cache.insert_h3(cache_key.clone(), h3_port);
|
debug!(h3_port, url, "Backend advertises H3 via Alt-Svc");
|
||||||
|
let reason = format!("Alt-Svc response header ({})", url);
|
||||||
|
self.protocol_cache.insert_h3(cache_key.clone(), h3_port, &reason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2569,7 +2767,7 @@ impl HttpProxyService {
|
|||||||
let connecting = self.quinn_client_endpoint.connect(addr, &server_name)?;
|
let connecting = self.quinn_client_endpoint.connect(addr, &server_name)?;
|
||||||
|
|
||||||
let connection = tokio::time::timeout(QUIC_CONNECT_TIMEOUT, connecting).await
|
let connection = tokio::time::timeout(QUIC_CONNECT_TIMEOUT, connecting).await
|
||||||
.map_err(|_| "QUIC connect timeout (3s)")??;
|
.map_err(|_| format!("QUIC connect timeout (3s) for {}", host))??;
|
||||||
|
|
||||||
debug!("QUIC backend connection established to {}:{}", host, port);
|
debug!("QUIC backend connection established to {}:{}", host, port);
|
||||||
Ok(connection)
|
Ok(connection)
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ pub struct Metrics {
|
|||||||
pub total_udp_sessions: u64,
|
pub total_udp_sessions: u64,
|
||||||
pub total_datagrams_in: u64,
|
pub total_datagrams_in: u64,
|
||||||
pub total_datagrams_out: u64,
|
pub total_datagrams_out: u64,
|
||||||
|
// Protocol detection cache snapshot (populated by RustProxy from HttpProxyService)
|
||||||
|
pub detected_protocols: Vec<ProtocolCacheEntryMetric>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Per-route metrics.
|
/// Per-route metrics.
|
||||||
@@ -76,6 +78,27 @@ pub struct BackendMetrics {
|
|||||||
pub h2_failures: u64,
|
pub h2_failures: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Protocol cache entry for metrics/UI display.
|
||||||
|
/// Populated from the HTTP proxy service's protocol detection cache.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ProtocolCacheEntryMetric {
|
||||||
|
pub host: String,
|
||||||
|
pub port: u16,
|
||||||
|
pub domain: Option<String>,
|
||||||
|
pub protocol: String,
|
||||||
|
pub h3_port: Option<u16>,
|
||||||
|
pub age_secs: u64,
|
||||||
|
pub last_accessed_secs: u64,
|
||||||
|
pub last_probed_secs: u64,
|
||||||
|
pub h2_suppressed: bool,
|
||||||
|
pub h3_suppressed: bool,
|
||||||
|
pub h2_cooldown_remaining_secs: Option<u64>,
|
||||||
|
pub h3_cooldown_remaining_secs: Option<u64>,
|
||||||
|
pub h2_consecutive_failures: Option<u32>,
|
||||||
|
pub h3_consecutive_failures: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Statistics snapshot.
|
/// Statistics snapshot.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@@ -824,6 +847,7 @@ impl MetricsCollector {
|
|||||||
total_udp_sessions: self.total_udp_sessions.load(Ordering::Relaxed),
|
total_udp_sessions: self.total_udp_sessions.load(Ordering::Relaxed),
|
||||||
total_datagrams_in: self.total_datagrams_in.load(Ordering::Relaxed),
|
total_datagrams_in: self.total_datagrams_in.load(Ordering::Relaxed),
|
||||||
total_datagrams_out: self.total_datagrams_out.load(Ordering::Relaxed),
|
total_datagrams_out: self.total_datagrams_out.load(Ordering::Relaxed),
|
||||||
|
detected_protocols: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,13 @@ struct RelaySession {
|
|||||||
cancel: CancellationToken,
|
cancel: CancellationToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Drop for RelaySession {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.cancel.cancel();
|
||||||
|
self.return_task.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a QUIC endpoint with a PROXY protocol v2 relay layer.
|
/// Create a QUIC endpoint with a PROXY protocol v2 relay layer.
|
||||||
///
|
///
|
||||||
/// Instead of giving the external socket to quinn, we:
|
/// Instead of giving the external socket to quinn, we:
|
||||||
@@ -634,7 +641,7 @@ async fn forward_quic_stream_to_tcp(
|
|||||||
let la_watch = Arc::clone(&last_activity);
|
let la_watch = Arc::clone(&last_activity);
|
||||||
let c2b_abort = c2b.abort_handle();
|
let c2b_abort = c2b.abort_handle();
|
||||||
let b2c_abort = b2c.abort_handle();
|
let b2c_abort = b2c.abort_handle();
|
||||||
tokio::spawn(async move {
|
let watchdog = tokio::spawn(async move {
|
||||||
let check_interval = std::time::Duration::from_secs(5);
|
let check_interval = std::time::Duration::from_secs(5);
|
||||||
let mut last_seen = 0u64;
|
let mut last_seen = 0u64;
|
||||||
loop {
|
loop {
|
||||||
@@ -665,6 +672,7 @@ async fn forward_quic_stream_to_tcp(
|
|||||||
|
|
||||||
let bytes_in = c2b.await.unwrap_or(0);
|
let bytes_in = c2b.await.unwrap_or(0);
|
||||||
let bytes_out = b2c.await.unwrap_or(0);
|
let bytes_out = b2c.await.unwrap_or(0);
|
||||||
|
watchdog.abort();
|
||||||
|
|
||||||
Ok((bytes_in, bytes_out))
|
Ok((bytes_in, bytes_out))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -937,8 +937,31 @@ impl RustProxy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get current metrics snapshot.
|
/// Get current metrics snapshot.
|
||||||
|
/// Includes protocol cache entries from the HTTP proxy service.
|
||||||
pub fn get_metrics(&self) -> Metrics {
|
pub fn get_metrics(&self) -> Metrics {
|
||||||
self.metrics.snapshot()
|
let mut metrics = self.metrics.snapshot();
|
||||||
|
if let Some(ref lm) = self.listener_manager {
|
||||||
|
let entries = lm.http_proxy().protocol_cache_snapshot();
|
||||||
|
metrics.detected_protocols = entries.into_iter().map(|e| {
|
||||||
|
rustproxy_metrics::ProtocolCacheEntryMetric {
|
||||||
|
host: e.host,
|
||||||
|
port: e.port,
|
||||||
|
domain: e.domain,
|
||||||
|
protocol: e.protocol,
|
||||||
|
h3_port: e.h3_port,
|
||||||
|
age_secs: e.age_secs,
|
||||||
|
last_accessed_secs: e.last_accessed_secs,
|
||||||
|
last_probed_secs: e.last_probed_secs,
|
||||||
|
h2_suppressed: e.h2_suppressed,
|
||||||
|
h3_suppressed: e.h3_suppressed,
|
||||||
|
h2_cooldown_remaining_secs: e.h2_cooldown_remaining_secs,
|
||||||
|
h3_cooldown_remaining_secs: e.h3_cooldown_remaining_secs,
|
||||||
|
h2_consecutive_failures: e.h2_consecutive_failures,
|
||||||
|
h3_consecutive_failures: e.h3_consecutive_failures,
|
||||||
|
}
|
||||||
|
}).collect();
|
||||||
|
}
|
||||||
|
metrics
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a listening port at runtime.
|
/// Add a listening port at runtime.
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartproxy',
|
name: '@push.rocks/smartproxy',
|
||||||
version: '26.0.0',
|
version: '26.2.3',
|
||||||
description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
|
description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ export interface IMetrics {
|
|||||||
byBackend(): Map<string, IBackendMetrics>;
|
byBackend(): Map<string, IBackendMetrics>;
|
||||||
protocols(): Map<string, string>;
|
protocols(): Map<string, string>;
|
||||||
topByErrors(limit?: number): Array<{ backend: string; errors: number }>;
|
topByErrors(limit?: number): Array<{ backend: string; errors: number }>;
|
||||||
|
detectedProtocols(): IProtocolCacheEntry[];
|
||||||
};
|
};
|
||||||
|
|
||||||
// UDP metrics
|
// UDP metrics
|
||||||
@@ -113,6 +114,28 @@ export interface IMetricsConfig {
|
|||||||
prometheusPrefix: string; // Default: smartproxy_
|
prometheusPrefix: string; // Default: smartproxy_
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Protocol cache entry from the Rust proxy's auto-detection cache.
|
||||||
|
* Shows which protocol (h1/h2/h3) is detected for each backend+domain pair,
|
||||||
|
* including failure suppression state with escalating cooldowns.
|
||||||
|
*/
|
||||||
|
export interface IProtocolCacheEntry {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
domain: string | null;
|
||||||
|
protocol: string;
|
||||||
|
h3Port: number | null;
|
||||||
|
ageSecs: number;
|
||||||
|
lastAccessedSecs: number;
|
||||||
|
lastProbedSecs: number;
|
||||||
|
h2Suppressed: boolean;
|
||||||
|
h3Suppressed: boolean;
|
||||||
|
h2CooldownRemainingSecs: number | null;
|
||||||
|
h3CooldownRemainingSecs: number | null;
|
||||||
|
h2ConsecutiveFailures: number | null;
|
||||||
|
h3ConsecutiveFailures: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Per-backend metrics
|
* Per-backend metrics
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { IMetrics, IBackendMetrics, IThroughputData, IThroughputHistoryPoint } from './models/metrics-types.js';
|
import type { IMetrics, IBackendMetrics, IProtocolCacheEntry, IThroughputData, IThroughputHistoryPoint } from './models/metrics-types.js';
|
||||||
import type { RustProxyBridge } from './rust-proxy-bridge.js';
|
import type { RustProxyBridge } from './rust-proxy-bridge.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -216,6 +216,9 @@ export class RustMetricsAdapter implements IMetrics {
|
|||||||
result.sort((a, b) => b.errors - a.errors);
|
result.sort((a, b) => b.errors - a.errors);
|
||||||
return result.slice(0, limit);
|
return result.slice(0, limit);
|
||||||
},
|
},
|
||||||
|
detectedProtocols: (): IProtocolCacheEntry[] => {
|
||||||
|
return this.cache?.detectedProtocols ?? [];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
public udp = {
|
public udp = {
|
||||||
|
|||||||
@@ -69,14 +69,15 @@ export function createOffsetPortMappingRoute(options: {
|
|||||||
priority?: number;
|
priority?: number;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}): IRouteConfig {
|
}): IRouteConfig {
|
||||||
|
const { ports, targetHost, offset, name, domains, priority, ...rest } = options;
|
||||||
return createPortMappingRoute({
|
return createPortMappingRoute({
|
||||||
sourcePortRange: options.ports,
|
sourcePortRange: ports,
|
||||||
targetHost: options.targetHost,
|
targetHost,
|
||||||
portMapper: (context) => context.port + options.offset,
|
portMapper: (context) => context.port + offset,
|
||||||
name: options.name || `Offset Mapping (${options.offset > 0 ? '+' : ''}${options.offset}) for ${options.domains || 'all domains'}`,
|
name: name || `Offset Mapping (${offset > 0 ? '+' : ''}${offset}) for ${domains || 'all domains'}`,
|
||||||
domains: options.domains,
|
domains,
|
||||||
priority: options.priority,
|
priority,
|
||||||
...options
|
...rest
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -258,8 +258,10 @@ export class RouteValidator {
|
|||||||
errorMap.set(route.name, existingErrors);
|
errorMap.set(route.name, existingErrors);
|
||||||
valid = false;
|
valid = false;
|
||||||
}
|
}
|
||||||
|
if (route.name) {
|
||||||
routeNames.add(route.name);
|
routeNames.add(route.name);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Validate each route
|
// Validate each route
|
||||||
for (const route of routes) {
|
for (const route of routes) {
|
||||||
@@ -328,7 +330,7 @@ export class RouteValidator {
|
|||||||
if (catchAllRoutes.length > 1) {
|
if (catchAllRoutes.length > 1) {
|
||||||
for (const route of catchAllRoutes) {
|
for (const route of catchAllRoutes) {
|
||||||
conflicts.push({
|
conflicts.push({
|
||||||
route: route.name,
|
route: route.name || 'unnamed',
|
||||||
message: `Multiple catch-all routes on port ${port}`
|
message: `Multiple catch-all routes on port ${port}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,7 @@
|
|||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "NodeNext",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"verbatimModuleSyntax": true,
|
"verbatimModuleSyntax": true,
|
||||||
"baseUrl": ".",
|
"ignoreDeprecations": "6.0"
|
||||||
"paths": {}
|
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"dist_*/**/*.d.ts"
|
"dist_*/**/*.d.ts"
|
||||||
|
|||||||
Reference in New Issue
Block a user