Compare commits

..

10 Commits

Author SHA1 Message Date
c0e432fd9b v26.2.4
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-26 07:05:57 +00:00
a3d8a3a388 fix(rustproxy-http): improve HTTP/3 connection reuse and clean up stale proxy state 2026-03-26 07:05:57 +00:00
437d1a3329 v26.2.3
Some checks failed
Default (tags) / security (push) Failing after 3s
Default (tags) / test (push) Failing after 3s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-25 07:26:47 +00:00
746d93663d fix(repo): no changes to commit 2026-03-25 07:26:47 +00:00
a3f3fee253 v26.2.2
Some checks failed
Default (tags) / security (push) Failing after 4s
Default (tags) / test (push) Failing after 5s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-25 07:22:17 +00:00
53dee1fffc fix(proxy): improve connection cleanup and route validation handling 2026-03-25 07:22:17 +00:00
34dc0cb9b6 v26.2.1
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-23 11:11:55 +00:00
c83c43194b fix(rustproxy-http): include the upstream request URL when caching H3 Alt-Svc discoveries 2026-03-23 11:11:55 +00:00
d026d7c266 v26.2.0
Some checks failed
Default (tags) / security (push) Failing after 0s
Default (tags) / test (push) Failing after 0s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2026-03-23 09:42:07 +00:00
3b01144c51 feat(protocol-cache): add sliding TTL re-probing and eviction for backend protocol detection 2026-03-23 09:42:07 +00:00
25 changed files with 1094 additions and 885 deletions

View File

@@ -1,5 +1,40 @@
# Changelog # Changelog
## 2026-03-26 - 26.2.4 - fix(rustproxy-http)
improve HTTP/3 connection reuse and clean up stale proxy state
- Reuse pooled HTTP/3 SendRequest handles to skip repeated SETTINGS handshakes and reduce request overhead on QUIC pool hits
- Add periodic cleanup for per-route rate limiters and orphaned backend metrics to prevent unbounded memory growth after traffic or backend errors stop
- Enforce HTTP max connection lifetime alongside idle timeouts and apply configured lifetime values from the TCP listener
- Reduce HTTP/3 body copying by using owned Bytes paths for request and response streaming, and replace the custom response body adapter with a stream-based implementation
- Harden auxiliary proxy components by capping datagram handler buffer growth and removing duplicate RustProxy exit listeners
## 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) ## 2026-03-22 - 26.1.0 - feat(rustproxy-http)
add protocol failure suppression, h3 fallback escalation, and protocol cache metrics exposure add protocol failure suppression, h3 fallback escalation, and protocol cache metrics exposure

770
deno.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -1,6 +1,6 @@
{ {
"name": "@push.rocks/smartproxy", "name": "@push.rocks/smartproxy",
"version": "26.1.0", "version": "26.2.4",
"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
View File

@@ -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
rust/Cargo.lock generated
View File

@@ -1270,6 +1270,7 @@ dependencies = [
"arc-swap", "arc-swap",
"bytes", "bytes",
"dashmap", "dashmap",
"futures",
"h3", "h3",
"h3-quinn", "h3-quinn",
"http-body", "http-body",

View File

@@ -30,3 +30,4 @@ socket2 = { workspace = true }
quinn = { workspace = true } quinn = { workspace = true }
h3 = { workspace = true } h3 = { workspace = true }
h3-quinn = { workspace = true } h3-quinn = { workspace = true }
futures = { version = "0.3", default-features = false, features = ["std"] }

View File

@@ -56,7 +56,11 @@ struct PooledH2 {
} }
/// A pooled QUIC/HTTP/3 connection (multiplexed like H2). /// A pooled QUIC/HTTP/3 connection (multiplexed like H2).
/// Stores the h3 `SendRequest` handle so pool hits skip the h3 SETTINGS handshake.
pub struct PooledH3 { pub struct PooledH3 {
/// Multiplexed h3 request handle — clone to open a new stream.
pub send_request: h3::client::SendRequest<h3_quinn::OpenStreams, Bytes>,
/// Raw QUIC connection — kept for liveness probing (close_reason) only.
pub connection: quinn::Connection, pub connection: quinn::Connection,
pub created_at: Instant, pub created_at: Instant,
pub generation: u64, pub generation: u64,
@@ -197,7 +201,10 @@ impl ConnectionPool {
/// Try to get a pooled QUIC connection for the given key. /// Try to get a pooled QUIC connection for the given key.
/// QUIC connections are multiplexed — the connection is shared, not removed. /// QUIC connections are multiplexed — the connection is shared, not removed.
pub fn checkout_h3(&self, key: &PoolKey) -> Option<(quinn::Connection, Duration)> { pub fn checkout_h3(
&self,
key: &PoolKey,
) -> Option<(h3::client::SendRequest<h3_quinn::OpenStreams, Bytes>, quinn::Connection, Duration)> {
let entry = self.h3_pool.get(key)?; let entry = self.h3_pool.get(key)?;
let pooled = entry.value(); let pooled = entry.value();
let age = pooled.created_at.elapsed(); let age = pooled.created_at.elapsed();
@@ -215,13 +222,20 @@ impl ConnectionPool {
return None; return None;
} }
Some((pooled.connection.clone(), age)) Some((pooled.send_request.clone(), pooled.connection.clone(), age))
} }
/// Register a QUIC connection in the pool. Returns the generation ID. /// Register a QUIC connection and its h3 SendRequest handle in the pool.
pub fn register_h3(&self, key: PoolKey, connection: quinn::Connection) -> u64 { /// Returns the generation ID.
pub fn register_h3(
&self,
key: PoolKey,
connection: quinn::Connection,
send_request: h3::client::SendRequest<h3_quinn::OpenStreams, Bytes>,
) -> u64 {
let gen = self.h2_generation.fetch_add(1, Ordering::Relaxed); let gen = self.h2_generation.fetch_add(1, Ordering::Relaxed);
self.h3_pool.insert(key, PooledH3 { self.h3_pool.insert(key, PooledH3 {
send_request,
connection, connection,
created_at: Instant::now(), created_at: Instant::now(),
generation: gen, generation: gen,

View File

@@ -116,7 +116,7 @@ async fn handle_h3_request(
cancel: CancellationToken, cancel: CancellationToken,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
// Stream request body from H3 client via an mpsc channel. // Stream request body from H3 client via an mpsc channel.
let (body_tx, body_rx) = tokio::sync::mpsc::channel::<Bytes>(4); let (body_tx, body_rx) = tokio::sync::mpsc::channel::<Bytes>(32);
// Spawn the H3 body reader task with cancellation // Spawn the H3 body reader task with cancellation
let body_cancel = cancel.clone(); let body_cancel = cancel.clone();
@@ -132,8 +132,7 @@ async fn handle_h3_request(
} }
}; };
let mut chunk = chunk; let mut chunk = chunk;
let data = Bytes::copy_from_slice(chunk.chunk()); let data = chunk.copy_to_bytes(chunk.remaining());
chunk.advance(chunk.remaining());
if body_tx.send(data).await.is_err() { if body_tx.send(data).await.is_err() {
break; break;
} }
@@ -179,8 +178,8 @@ async fn handle_h3_request(
while let Some(frame) = resp_body.frame().await { while let Some(frame) = resp_body.frame().await {
match frame { match frame {
Ok(frame) => { Ok(frame) => {
if let Some(data) = frame.data_ref() { if let Ok(data) = frame.into_data() {
stream.send_data(Bytes::copy_from_slice(data)).await stream.send_data(data).await
.map_err(|e| anyhow::anyhow!("Failed to send H3 data: {}", e))?; .map_err(|e| anyhow::anyhow!("Failed to send H3 data: {}", e))?;
} }
} }

View File

@@ -1,20 +1,36 @@
//! Bounded, TTL-based protocol detection cache with generic failure suppression. //! 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.
//! //!
//! ## Sliding TTL
//!
//! 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 //! ## Upgrade signals
//! //!
//! - ALPN (TLS handshake) → detects H2 vs H1 //! - ALPN (TLS handshake) → detects H2 vs H1
//! - Alt-Svc (response header) → advertises H3 //! - 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 //! ## Failure suppression
//! //!
//! When a protocol fails, `record_failure()` prevents upgrade signals from //! When a protocol fails, `record_failure()` prevents upgrade signals from
//! re-introducing it until an escalating cooldown expires (5s → 10s → ... → 300s). //! re-introducing it until an escalating cooldown expires (5s → 10s → ... → 300s).
//! Within-request escalation is allowed via `can_retry()` after a 5s minimum gap. //! 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 //! Cascading: when a lower protocol also fails, higher protocol cooldowns are
//! reduced to 5s remaining (not instant clear), preventing tight retry loops. //! reduced to 5s remaining (not instant clear), preventing tight retry loops.
@@ -22,11 +38,17 @@ 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.
const PROTOCOL_CACHE_MAX_ENTRIES: usize = 4096; const PROTOCOL_CACHE_MAX_ENTRIES: usize = 4096;
@@ -37,7 +59,7 @@ const PROTOCOL_CACHE_CLEANUP_INTERVAL: Duration = Duration::from_secs(60);
/// Minimum cooldown between retry attempts of a failed protocol. /// Minimum cooldown between retry attempts of a failed protocol.
const PROTOCOL_FAILURE_COOLDOWN: Duration = Duration::from_secs(5); const PROTOCOL_FAILURE_COOLDOWN: Duration = Duration::from_secs(5);
/// Maximum cooldown (escalation ceiling). Matches cache TTL. /// Maximum cooldown (escalation ceiling).
const PROTOCOL_FAILURE_MAX_COOLDOWN: Duration = Duration::from_secs(300); const PROTOCOL_FAILURE_MAX_COOLDOWN: Duration = Duration::from_secs(300);
/// Consecutive failure count at which cooldown reaches maximum. /// Consecutive failure count at which cooldown reaches maximum.
@@ -52,12 +74,26 @@ pub enum DetectedProtocol {
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).
@@ -70,10 +106,15 @@ 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>,
} }
@@ -138,6 +179,8 @@ pub struct ProtocolCacheEntry {
pub protocol: String, pub protocol: String,
pub h3_port: Option<u16>, pub h3_port: Option<u16>,
pub age_secs: u64, pub age_secs: u64,
pub last_accessed_secs: u64,
pub last_probed_secs: u64,
pub h2_suppressed: bool, pub h2_suppressed: bool,
pub h3_suppressed: bool, pub h3_suppressed: bool,
pub h2_cooldown_remaining_secs: Option<u64>, pub h2_cooldown_remaining_secs: Option<u64>,
@@ -154,11 +197,11 @@ fn escalate_cooldown(consecutive: u32) -> Duration {
Duration::from_secs(secs.min(PROTOCOL_FAILURE_MAX_COOLDOWN.as_secs())) Duration::from_secs(secs.min(PROTOCOL_FAILURE_MAX_COOLDOWN.as_secs()))
} }
/// Bounded, TTL-based protocol detection cache with failure suppression. /// 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.
@@ -190,15 +233,25 @@ impl ProtocolCache {
} }
/// 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: {:?})", // Refresh sliding TTL
entry.protocol, key.host, key.port, key.requested_host); 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
@@ -214,7 +267,7 @@ impl ProtocolCache {
/// **Key semantic**: only suppresses if the protocol being inserted matches /// **Key semantic**: only suppresses if the protocol being inserted matches
/// a suppressed protocol. H1 inserts are NEVER suppressed — downgrades /// a suppressed protocol. H1 inserts are NEVER suppressed — downgrades
/// always succeed. /// always succeed.
pub fn insert(&self, key: ProtocolCacheKey, protocol: DetectedProtocol) -> bool { pub fn insert(&self, key: ProtocolCacheKey, protocol: DetectedProtocol, reason: &str) -> bool {
if self.is_suppressed(&key, protocol) { if self.is_suppressed(&key, protocol) {
debug!( debug!(
host = %key.host, port = %key.port, domain = ?key.requested_host, host = %key.host, port = %key.port, domain = ?key.requested_host,
@@ -223,13 +276,13 @@ impl ProtocolCache {
); );
return false; return false;
} }
self.insert_internal(key, protocol, None); self.insert_internal(key, protocol, None, reason);
true true
} }
/// Insert an H3 detection result with the Alt-Svc advertised port. /// Insert an H3 detection result with the Alt-Svc advertised port.
/// Returns `false` if H3 is suppressed. /// Returns `false` if H3 is suppressed.
pub fn insert_h3(&self, key: ProtocolCacheKey, h3_port: u16) -> bool { pub fn insert_h3(&self, key: ProtocolCacheKey, h3_port: u16, reason: &str) -> bool {
if self.is_suppressed(&key, DetectedProtocol::H3) { if self.is_suppressed(&key, DetectedProtocol::H3) {
debug!( debug!(
host = %key.host, port = %key.port, domain = ?key.requested_host, host = %key.host, port = %key.port, domain = ?key.requested_host,
@@ -237,10 +290,54 @@ impl ProtocolCache {
); );
return false; return false;
} }
self.insert_internal(key, DetectedProtocol::H3, Some(h3_port)); self.insert_internal(key, DetectedProtocol::H3, Some(h3_port), reason);
true true
} }
/// Update the cache after an inline ALPN re-probe completes.
///
/// Always updates `last_probed_at`. If the protocol changed, logs the transition
/// and updates the entry. Returns `Some(new_protocol)` if changed, `None` if unchanged.
pub fn update_probe_result(
&self,
key: &ProtocolCacheKey,
probed_protocol: DetectedProtocol,
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)
}
}
/// Record a protocol failure. Future `insert()` calls for this protocol /// Record a protocol failure. Future `insert()` calls for this protocol
/// will be suppressed until the escalating cooldown expires. /// will be suppressed until the escalating cooldown expires.
/// ///
@@ -281,7 +378,7 @@ impl ProtocolCache {
Self::reduce_cooldown_to(entry.h3.as_mut(), PROTOCOL_FAILURE_COOLDOWN); Self::reduce_cooldown_to(entry.h3.as_mut(), PROTOCOL_FAILURE_COOLDOWN);
} }
debug!( info!(
host = %key.host, port = %key.port, domain = ?key.requested_host, host = %key.host, port = %key.port, domain = ?key.requested_host,
protocol = ?protocol, protocol = ?protocol,
consecutive = consecutive, consecutive = consecutive,
@@ -348,6 +445,17 @@ impl ProtocolCache {
} }
} }
/// 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();
@@ -357,7 +465,7 @@ impl ProtocolCache {
/// Snapshot all non-expired cache entries for metrics/UI display. /// Snapshot all non-expired cache entries for metrics/UI display.
pub fn snapshot(&self) -> Vec<ProtocolCacheEntry> { pub fn snapshot(&self) -> Vec<ProtocolCacheEntry> {
self.cache.iter() self.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| { .map(|entry| {
let key = entry.key(); let key = entry.key();
let val = entry.value(); let val = entry.value();
@@ -381,6 +489,8 @@ impl ProtocolCache {
}, },
h3_port: val.h3_port, h3_port: val.h3_port,
age_secs: val.detected_at.elapsed().as_secs(), 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, h2_suppressed: h2_sup,
h3_suppressed: h3_sup, h3_suppressed: h3_sup,
h2_cooldown_remaining_secs: h2_cd, h2_cooldown_remaining_secs: h2_cd,
@@ -395,19 +505,37 @@ impl ProtocolCache {
// --- Internal helpers --- // --- Internal helpers ---
/// Insert a protocol detection result with an optional H3 port. /// 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. /// No suppression check — callers must check before calling.
fn insert_internal(&self, key: ProtocolCacheKey, protocol: DetectedProtocol, h3_port: Option<u16>) { 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) { if self.cache.len() >= PROTOCOL_CACHE_MAX_ENTRIES && !self.cache.contains_key(&key) {
let oldest = self.cache.iter() let oldest = self.cache.iter()
.min_by_key(|entry| entry.value().detected_at) .min_by_key(|entry| entry.value().last_accessed_at)
.map(|entry| entry.key().clone()); .map(|entry| entry.key().clone());
if let Some(oldest_key) = oldest { if let Some(oldest_key) = oldest {
self.cache.remove(&oldest_key); self.cache.remove(&oldest_key);
} }
} }
let now = Instant::now();
self.cache.insert(key, CachedEntry { self.cache.insert(key, CachedEntry {
protocol, protocol,
detected_at: Instant::now(), detected_at: now,
last_accessed_at: now,
last_probed_at: now,
h3_port, h3_port,
}); });
} }
@@ -453,9 +581,9 @@ impl ProtocolCache {
loop { loop {
interval.tick().await; interval.tick().await;
// Clean expired cache entries // 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();

View File

@@ -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,
} }
} }
} }
@@ -69,15 +72,16 @@ const DEFAULT_CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_s
/// If no new request arrives within this duration, the connection is closed. /// If no new request arrives within this duration, the connection is closed.
const DEFAULT_HTTP_IDLE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60); const DEFAULT_HTTP_IDLE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60);
/// Default HTTP max connection lifetime (1 hour).
/// HTTP connections are forcefully closed after this duration regardless of activity.
const DEFAULT_HTTP_MAX_LIFETIME: std::time::Duration = std::time::Duration::from_secs(3600);
/// Default WebSocket inactivity timeout (1 hour). /// Default WebSocket inactivity timeout (1 hour).
const DEFAULT_WS_INACTIVITY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3600); const DEFAULT_WS_INACTIVITY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3600);
/// Default WebSocket max lifetime (24 hours). /// Default WebSocket max lifetime (24 hours).
const DEFAULT_WS_MAX_LIFETIME: std::time::Duration = std::time::Duration::from_secs(86400); const DEFAULT_WS_MAX_LIFETIME: std::time::Duration = std::time::Duration::from_secs(86400);
/// Timeout for QUIC (H3) backend connections. Short because UDP is often firewalled.
const QUIC_CONNECT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3);
/// Protocol decision for backend connection. /// Protocol decision for backend connection.
#[derive(Debug)] #[derive(Debug)]
enum ProtocolDecision { enum ProtocolDecision {
@@ -219,6 +223,8 @@ pub struct HttpProxyService {
protocol_cache: Arc<crate::protocol_cache::ProtocolCache>, protocol_cache: Arc<crate::protocol_cache::ProtocolCache>,
/// HTTP keep-alive idle timeout: close connection if no new request arrives within this duration. /// HTTP keep-alive idle timeout: close connection if no new request arrives within this duration.
http_idle_timeout: std::time::Duration, http_idle_timeout: std::time::Duration,
/// HTTP max connection lifetime: forcefully close connection after this duration regardless of activity.
http_max_lifetime: std::time::Duration,
/// WebSocket inactivity timeout (no data in either direction). /// WebSocket inactivity timeout (no data in either direction).
ws_inactivity_timeout: std::time::Duration, ws_inactivity_timeout: std::time::Duration,
/// WebSocket maximum connection lifetime. /// WebSocket maximum connection lifetime.
@@ -245,6 +251,7 @@ impl HttpProxyService {
connection_pool: Arc::new(crate::connection_pool::ConnectionPool::new()), connection_pool: Arc::new(crate::connection_pool::ConnectionPool::new()),
protocol_cache: Arc::new(crate::protocol_cache::ProtocolCache::new()), protocol_cache: Arc::new(crate::protocol_cache::ProtocolCache::new()),
http_idle_timeout: DEFAULT_HTTP_IDLE_TIMEOUT, http_idle_timeout: DEFAULT_HTTP_IDLE_TIMEOUT,
http_max_lifetime: DEFAULT_HTTP_MAX_LIFETIME,
ws_inactivity_timeout: DEFAULT_WS_INACTIVITY_TIMEOUT, ws_inactivity_timeout: DEFAULT_WS_INACTIVITY_TIMEOUT,
ws_max_lifetime: DEFAULT_WS_MAX_LIFETIME, ws_max_lifetime: DEFAULT_WS_MAX_LIFETIME,
quinn_client_endpoint: Arc::new(Self::create_quinn_client_endpoint()), quinn_client_endpoint: Arc::new(Self::create_quinn_client_endpoint()),
@@ -272,21 +279,24 @@ impl HttpProxyService {
connection_pool: Arc::new(crate::connection_pool::ConnectionPool::new()), connection_pool: Arc::new(crate::connection_pool::ConnectionPool::new()),
protocol_cache: Arc::new(crate::protocol_cache::ProtocolCache::new()), protocol_cache: Arc::new(crate::protocol_cache::ProtocolCache::new()),
http_idle_timeout: DEFAULT_HTTP_IDLE_TIMEOUT, http_idle_timeout: DEFAULT_HTTP_IDLE_TIMEOUT,
http_max_lifetime: DEFAULT_HTTP_MAX_LIFETIME,
ws_inactivity_timeout: DEFAULT_WS_INACTIVITY_TIMEOUT, ws_inactivity_timeout: DEFAULT_WS_INACTIVITY_TIMEOUT,
ws_max_lifetime: DEFAULT_WS_MAX_LIFETIME, ws_max_lifetime: DEFAULT_WS_MAX_LIFETIME,
quinn_client_endpoint: Arc::new(Self::create_quinn_client_endpoint()), quinn_client_endpoint: Arc::new(Self::create_quinn_client_endpoint()),
} }
} }
/// Set the HTTP keep-alive idle timeout, WebSocket inactivity timeout, and /// Set the HTTP keep-alive idle timeout, HTTP max lifetime, WebSocket inactivity
/// WebSocket max lifetime from connection config values. /// timeout, and WebSocket max lifetime from connection config values.
pub fn set_connection_timeouts( pub fn set_connection_timeouts(
&mut self, &mut self,
http_idle_timeout: std::time::Duration, http_idle_timeout: std::time::Duration,
http_max_lifetime: std::time::Duration,
ws_inactivity_timeout: std::time::Duration, ws_inactivity_timeout: std::time::Duration,
ws_max_lifetime: std::time::Duration, ws_max_lifetime: std::time::Duration,
) { ) {
self.http_idle_timeout = http_idle_timeout; self.http_idle_timeout = http_idle_timeout;
self.http_max_lifetime = http_max_lifetime;
self.ws_inactivity_timeout = ws_inactivity_timeout; self.ws_inactivity_timeout = ws_inactivity_timeout;
self.ws_max_lifetime = ws_max_lifetime; self.ws_max_lifetime = ws_max_lifetime;
} }
@@ -311,6 +321,15 @@ impl HttpProxyService {
self.protocol_cache.clear(); self.protocol_cache.clear();
} }
/// Clean up expired entries in all per-route rate limiters.
/// Called from the background sampling task to prevent unbounded growth
/// when traffic stops after a burst of unique IPs.
pub fn cleanup_all_rate_limiters(&self) {
for entry in self.route_rate_limiters.iter() {
entry.value().cleanup();
}
}
/// Snapshot the protocol cache for metrics/UI display. /// Snapshot the protocol cache for metrics/UI display.
pub fn protocol_cache_snapshot(&self) -> Vec<crate::protocol_cache::ProtocolCacheEntry> { pub fn protocol_cache_snapshot(&self) -> Vec<crate::protocol_cache::ProtocolCacheEntry> {
self.protocol_cache.snapshot() self.protocol_cache.snapshot()
@@ -351,6 +370,7 @@ impl HttpProxyService {
// Capture timeouts before `self` is moved into the service closure. // Capture timeouts before `self` is moved into the service closure.
let idle_timeout = self.http_idle_timeout; let idle_timeout = self.http_idle_timeout;
let max_lifetime = self.http_max_lifetime;
// Activity tracker: updated at the START and END of each request. // Activity tracker: updated at the START and END of each request.
// The idle watchdog checks this to determine if the connection is idle // The idle watchdog checks this to determine if the connection is idle
@@ -371,7 +391,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;
@@ -409,15 +429,23 @@ impl HttpProxyService {
} }
} }
_ = async { _ = async {
// Idle watchdog: check every 5s whether the connection has been idle // Idle + lifetime watchdog: check every 5s whether the connection has been
// (no active requests AND no activity for idle_timeout). // idle (no active requests AND no activity for idle_timeout) or exceeded
// This avoids killing long-running requests or upgraded connections. // the max connection lifetime.
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 {
tokio::time::sleep(check_interval).await; tokio::time::sleep(check_interval).await;
// Never close while a request is in progress // Check max connection lifetime (unconditional — even active connections
// must eventually be recycled to prevent resource accumulation).
if start.elapsed() >= max_lifetime {
debug!("HTTP connection exceeded max lifetime ({}s) from {}",
max_lifetime.as_secs(), peer_addr);
return;
}
// Never close for idleness while a request is in progress
if active_requests.load(Ordering::Relaxed) > 0 { if active_requests.load(Ordering::Relaxed) > 0 {
last_seen = last_activity.load(Ordering::Relaxed); last_seen = last_activity.load(Ordering::Relaxed);
continue; continue;
@@ -434,7 +462,7 @@ impl HttpProxyService {
last_seen = current; last_seen = current;
} }
} => { } => {
debug!("HTTP connection idle timeout ({}s) from {}", idle_timeout.as_secs(), peer_addr); debug!("HTTP connection timeout from {}", peer_addr);
conn.as_mut().graceful_shutdown(); conn.as_mut().graceful_shutdown();
// Give any in-flight work 5s to drain after graceful shutdown // Give any in-flight work 5s to drain after graceful shutdown
let _ = tokio::time::timeout(std::time::Duration::from_secs(5), conn).await; let _ = tokio::time::timeout(std::time::Duration::from_secs(5), conn).await;
@@ -711,6 +739,9 @@ impl HttpProxyService {
let cached_h3_port = self.protocol_cache.get(&protocol_cache_key) let cached_h3_port = self.protocol_cache.get(&protocol_cache_key)
.and_then(|c| c.h3_port); .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,
@@ -721,6 +752,12 @@ 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 self.protocol_cache.is_suppressed(&protocol_cache_key, crate::protocol_cache::DetectedProtocol::H3) { if self.protocol_cache.is_suppressed(&protocol_cache_key, crate::protocol_cache::DetectedProtocol::H3) {
@@ -766,6 +803,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 ---
@@ -778,10 +816,10 @@ impl HttpProxyService {
}; };
// Try H3 pool checkout first // Try H3 pool checkout first
if let Some((quic_conn, _age)) = self.connection_pool.checkout_h3(&h3_pool_key) { if let Some((pooled_sr, quic_conn, _age)) = self.connection_pool.checkout_h3(&h3_pool_key) {
self.metrics.backend_pool_hit(&upstream_key); self.metrics.backend_pool_hit(&upstream_key);
let result = self.forward_h3( let result = self.forward_h3(
quic_conn, parts, body, upstream_headers, &upstream_path, quic_conn, Some(pooled_sr), parts, body, upstream_headers, &upstream_path,
route_match.route, route_id, &ip_str, &h3_pool_key, domain_str, &conn_activity, &upstream_key, route_match.route, route_id, &ip_str, &h3_pool_key, domain_str, &conn_activity, &upstream_key,
).await; ).await;
self.upstream_selector.connection_ended(&upstream_key); self.upstream_selector.connection_ended(&upstream_key);
@@ -794,7 +832,7 @@ impl HttpProxyService {
self.metrics.backend_pool_miss(&upstream_key); self.metrics.backend_pool_miss(&upstream_key);
self.metrics.backend_connection_opened(&upstream_key, std::time::Instant::now().elapsed()); self.metrics.backend_connection_opened(&upstream_key, std::time::Instant::now().elapsed());
let result = self.forward_h3( let result = self.forward_h3(
quic_conn, parts, body, upstream_headers, &upstream_path, quic_conn, None, parts, body, upstream_headers, &upstream_path,
route_match.route, route_id, &ip_str, &h3_pool_key, domain_str, &conn_activity, &upstream_key, route_match.route, route_id, &ip_str, &h3_pool_key, domain_str, &conn_activity, &upstream_key,
).await; ).await;
self.upstream_selector.connection_ended(&upstream_key); self.upstream_selector.connection_ended(&upstream_key);
@@ -893,7 +931,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,
@@ -904,13 +942,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"
); );
@@ -938,17 +981,17 @@ impl HttpProxyService {
if let Some(h3_port) = cached_h3_port { if let Some(h3_port) = cached_h3_port {
if self.protocol_cache.can_retry(&protocol_cache_key, crate::protocol_cache::DetectedProtocol::H3) { 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); 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"); debug!(backend = %upstream_key, domain = %domain_str, "TLS connect failed — escalating to H3");
match self.connect_quic_backend(&upstream.host, h3_port).await { match self.connect_quic_backend(&upstream.host, h3_port).await {
Ok(quic_conn) => { Ok(quic_conn) => {
self.protocol_cache.clear_failure(&protocol_cache_key, crate::protocol_cache::DetectedProtocol::H3); self.protocol_cache.clear_failure(&protocol_cache_key, crate::protocol_cache::DetectedProtocol::H3);
self.protocol_cache.insert_h3(protocol_cache_key.clone(), h3_port); self.protocol_cache.insert_h3(protocol_cache_key.clone(), h3_port, "recovery — TLS failed, H3 succeeded");
let h3_pool_key = crate::connection_pool::PoolKey { let h3_pool_key = crate::connection_pool::PoolKey {
host: upstream.host.clone(), port: h3_port, use_tls: true, host: upstream.host.clone(), port: h3_port, use_tls: true,
protocol: crate::connection_pool::PoolProtocol::H3, protocol: crate::connection_pool::PoolProtocol::H3,
}; };
let result = self.forward_h3( let result = self.forward_h3(
quic_conn, parts, body, upstream_headers, &upstream_path, quic_conn, None, parts, body, upstream_headers, &upstream_path,
route_match.route, route_id, &ip_str, &h3_pool_key, domain_str, &conn_activity, &upstream_key, route_match.route, route_id, &ip_str, &h3_pool_key, domain_str, &conn_activity, &upstream_key,
).await; ).await;
self.upstream_selector.connection_ended(&upstream_key); self.upstream_selector.connection_ended(&upstream_key);
@@ -961,6 +1004,8 @@ impl HttpProxyService {
} }
} }
} }
// 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"));
} }
@@ -979,17 +1024,17 @@ impl HttpProxyService {
if let Some(h3_port) = cached_h3_port { if let Some(h3_port) = cached_h3_port {
if self.protocol_cache.can_retry(&protocol_cache_key, crate::protocol_cache::DetectedProtocol::H3) { 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); 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"); debug!(backend = %upstream_key, domain = %domain_str, "TLS connect timeout — escalating to H3");
match self.connect_quic_backend(&upstream.host, h3_port).await { match self.connect_quic_backend(&upstream.host, h3_port).await {
Ok(quic_conn) => { Ok(quic_conn) => {
self.protocol_cache.clear_failure(&protocol_cache_key, crate::protocol_cache::DetectedProtocol::H3); self.protocol_cache.clear_failure(&protocol_cache_key, crate::protocol_cache::DetectedProtocol::H3);
self.protocol_cache.insert_h3(protocol_cache_key.clone(), h3_port); self.protocol_cache.insert_h3(protocol_cache_key.clone(), h3_port, "recovery — TLS timeout, H3 succeeded");
let h3_pool_key = crate::connection_pool::PoolKey { let h3_pool_key = crate::connection_pool::PoolKey {
host: upstream.host.clone(), port: h3_port, use_tls: true, host: upstream.host.clone(), port: h3_port, use_tls: true,
protocol: crate::connection_pool::PoolProtocol::H3, protocol: crate::connection_pool::PoolProtocol::H3,
}; };
let result = self.forward_h3( let result = self.forward_h3(
quic_conn, parts, body, upstream_headers, &upstream_path, quic_conn, None, parts, body, upstream_headers, &upstream_path,
route_match.route, route_id, &ip_str, &h3_pool_key, domain_str, &conn_activity, &upstream_key, route_match.route, route_id, &ip_str, &h3_pool_key, domain_str, &conn_activity, &upstream_key,
).await; ).await;
self.upstream_selector.connection_ended(&upstream_key); self.upstream_selector.connection_ended(&upstream_key);
@@ -1002,6 +1047,8 @@ impl HttpProxyService {
} }
} }
} }
// 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"));
} }
@@ -1040,13 +1087,13 @@ impl HttpProxyService {
match self.connect_quic_backend(&upstream.host, h3_port).await { match self.connect_quic_backend(&upstream.host, h3_port).await {
Ok(quic_conn) => { Ok(quic_conn) => {
self.protocol_cache.clear_failure(&protocol_cache_key, crate::protocol_cache::DetectedProtocol::H3); self.protocol_cache.clear_failure(&protocol_cache_key, crate::protocol_cache::DetectedProtocol::H3);
self.protocol_cache.insert_h3(protocol_cache_key.clone(), h3_port); self.protocol_cache.insert_h3(protocol_cache_key.clone(), h3_port, "recovery — TCP failed, H3 succeeded");
let h3_pool_key = crate::connection_pool::PoolKey { let h3_pool_key = crate::connection_pool::PoolKey {
host: upstream.host.clone(), port: h3_port, use_tls: true, host: upstream.host.clone(), port: h3_port, use_tls: true,
protocol: crate::connection_pool::PoolProtocol::H3, protocol: crate::connection_pool::PoolProtocol::H3,
}; };
let result = self.forward_h3( let result = self.forward_h3(
quic_conn, parts, body, upstream_headers, &upstream_path, quic_conn, None, parts, body, upstream_headers, &upstream_path,
route_match.route, route_id, &ip_str, &h3_pool_key, domain_str, &conn_activity, &upstream_key, route_match.route, route_id, &ip_str, &h3_pool_key, domain_str, &conn_activity, &upstream_key,
).await; ).await;
self.upstream_selector.connection_ended(&upstream_key); self.upstream_selector.connection_ended(&upstream_key);
@@ -1059,6 +1106,8 @@ impl HttpProxyService {
} }
} }
} }
// 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"));
} }
@@ -1081,13 +1130,13 @@ impl HttpProxyService {
match self.connect_quic_backend(&upstream.host, h3_port).await { match self.connect_quic_backend(&upstream.host, h3_port).await {
Ok(quic_conn) => { Ok(quic_conn) => {
self.protocol_cache.clear_failure(&protocol_cache_key, crate::protocol_cache::DetectedProtocol::H3); self.protocol_cache.clear_failure(&protocol_cache_key, crate::protocol_cache::DetectedProtocol::H3);
self.protocol_cache.insert_h3(protocol_cache_key.clone(), h3_port); self.protocol_cache.insert_h3(protocol_cache_key.clone(), h3_port, "recovery — TCP timeout, H3 succeeded");
let h3_pool_key = crate::connection_pool::PoolKey { let h3_pool_key = crate::connection_pool::PoolKey {
host: upstream.host.clone(), port: h3_port, use_tls: true, host: upstream.host.clone(), port: h3_port, use_tls: true,
protocol: crate::connection_pool::PoolProtocol::H3, protocol: crate::connection_pool::PoolProtocol::H3,
}; };
let result = self.forward_h3( let result = self.forward_h3(
quic_conn, parts, body, upstream_headers, &upstream_path, quic_conn, None, parts, body, upstream_headers, &upstream_path,
route_match.route, route_id, &ip_str, &h3_pool_key, domain_str, &conn_activity, &upstream_key, route_match.route, route_id, &ip_str, &h3_pool_key, domain_str, &conn_activity, &upstream_key,
).await; ).await;
self.upstream_selector.connection_ended(&upstream_key); self.upstream_selector.connection_ended(&upstream_key);
@@ -1100,6 +1149,8 @@ impl HttpProxyService {
} }
} }
} }
// 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"));
} }
@@ -1183,8 +1234,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"),
_ => {}
} }
}); });
@@ -1574,7 +1627,7 @@ impl HttpProxyService {
cache_key.clone(), cache_key.clone(),
crate::protocol_cache::DetectedProtocol::H2, crate::protocol_cache::DetectedProtocol::H2,
); );
self.protocol_cache.insert(cache_key, crate::protocol_cache::DetectedProtocol::H1); 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) => {
@@ -1593,6 +1646,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"))
} }
} }
@@ -1717,7 +1772,7 @@ impl HttpProxyService {
cache_key.clone(), cache_key.clone(),
crate::protocol_cache::DetectedProtocol::H2, crate::protocol_cache::DetectedProtocol::H2,
); );
self.protocol_cache.insert(cache_key, crate::protocol_cache::DetectedProtocol::H1); 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 {
@@ -1738,6 +1793,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"))
} }
} }
@@ -1773,8 +1830,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"),
_ => {}
} }
}); });
@@ -1953,8 +2012,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);
} }
} }
} }
@@ -2708,7 +2769,12 @@ impl HttpProxyService {
let quic_crypto = quinn::crypto::rustls::QuicClientConfig::try_from(tls_config) let quic_crypto = quinn::crypto::rustls::QuicClientConfig::try_from(tls_config)
.expect("Failed to create QUIC client crypto config"); .expect("Failed to create QUIC client crypto config");
let client_config = quinn::ClientConfig::new(Arc::new(quic_crypto));
// Tune QUIC transport to match H2 flow-control: 2 MB per-stream receive window.
let mut transport = quinn::TransportConfig::default();
transport.stream_receive_window(quinn::VarInt::from_u32(2 * 1024 * 1024));
let mut client_config = quinn::ClientConfig::new(Arc::new(quic_crypto));
client_config.transport_config(Arc::new(transport));
let mut endpoint = quinn::Endpoint::client("0.0.0.0:0".parse().unwrap()) let mut endpoint = quinn::Endpoint::client("0.0.0.0:0".parse().unwrap())
.expect("Failed to create QUIC client endpoint"); .expect("Failed to create QUIC client endpoint");
@@ -2730,8 +2796,8 @@ impl HttpProxyService {
let server_name = host.to_string(); let server_name = host.to_string();
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(self.connect_timeout, connecting).await
.map_err(|_| format!("QUIC connect timeout (3s) for {}", host))??; .map_err(|_| format!("QUIC connect timeout ({:?}) for {}", self.connect_timeout, host))??;
debug!("QUIC backend connection established to {}:{}", host, port); debug!("QUIC backend connection established to {}:{}", host, port);
Ok(connection) Ok(connection)
@@ -2741,6 +2807,7 @@ impl HttpProxyService {
async fn forward_h3( async fn forward_h3(
&self, &self,
quic_conn: quinn::Connection, quic_conn: quinn::Connection,
pooled_sender: Option<h3::client::SendRequest<h3_quinn::OpenStreams, Bytes>>,
parts: hyper::http::request::Parts, parts: hyper::http::request::Parts,
body: BoxBody<Bytes, hyper::Error>, body: BoxBody<Bytes, hyper::Error>,
upstream_headers: hyper::HeaderMap, upstream_headers: hyper::HeaderMap,
@@ -2753,8 +2820,14 @@ impl HttpProxyService {
conn_activity: &ConnActivity, conn_activity: &ConnActivity,
backend_key: &str, backend_key: &str,
) -> Result<Response<BoxBody<Bytes, hyper::Error>>, hyper::Error> { ) -> Result<Response<BoxBody<Bytes, hyper::Error>>, hyper::Error> {
// Obtain the h3 SendRequest handle: skip handshake + driver on pool hit.
let (mut send_request, gen_holder) = if let Some(sr) = pooled_sender {
// Pool hit — reuse existing h3 session, no SETTINGS round-trip
(sr, None)
} else {
// Fresh QUIC connection — full h3 handshake + driver spawn
let h3_quinn_conn = h3_quinn::Connection::new(quic_conn.clone()); let h3_quinn_conn = h3_quinn::Connection::new(quic_conn.clone());
let (mut driver, mut send_request) = match h3::client::builder() let (mut driver, sr) = match h3::client::builder()
.send_grease(false) .send_grease(false)
.build(h3_quinn_conn) .build(h3_quinn_conn)
.await .await
@@ -2767,10 +2840,10 @@ impl HttpProxyService {
} }
}; };
// Spawn the h3 connection driver let gen_holder = Arc::new(std::sync::atomic::AtomicU64::new(u64::MAX));
{
let driver_pool = Arc::clone(&self.connection_pool); let driver_pool = Arc::clone(&self.connection_pool);
let driver_pool_key = pool_key.clone(); let driver_pool_key = pool_key.clone();
let gen_holder = Arc::new(std::sync::atomic::AtomicU64::new(u64::MAX));
let driver_gen = Arc::clone(&gen_holder); let driver_gen = Arc::clone(&gen_holder);
tokio::spawn(async move { tokio::spawn(async move {
let close_err = std::future::poll_fn(|cx| driver.poll_close(cx)).await; let close_err = std::future::poll_fn(|cx| driver.poll_close(cx)).await;
@@ -2780,6 +2853,9 @@ impl HttpProxyService {
driver_pool.remove_h3_if_generation(&driver_pool_key, g); driver_pool.remove_h3_if_generation(&driver_pool_key, g);
} }
}); });
}
(sr, Some(gen_holder))
};
// Build the H3 request // Build the H3 request
let uri = hyper::Uri::builder() let uri = hyper::Uri::builder()
@@ -2809,7 +2885,7 @@ impl HttpProxyService {
} }
}; };
// Stream request body // Stream request body (zero-copy: into_data yields owned Bytes)
let rid: Option<Arc<str>> = route_id.map(Arc::from); let rid: Option<Arc<str>> = route_id.map(Arc::from);
let sip: Arc<str> = Arc::from(source_ip); let sip: Arc<str> = Arc::from(source_ip);
@@ -2819,9 +2895,9 @@ impl HttpProxyService {
while let Some(frame) = body.frame().await { while let Some(frame) = body.frame().await {
match frame { match frame {
Ok(frame) => { Ok(frame) => {
if let Some(data) = frame.data_ref() { if let Ok(data) = frame.into_data() {
self.metrics.record_bytes(data.len() as u64, 0, rid.as_deref(), Some(&sip)); self.metrics.record_bytes(data.len() as u64, 0, rid.as_deref(), Some(&sip));
if let Err(e) = stream.send_data(Bytes::copy_from_slice(data)).await { if let Err(e) = stream.send_data(data).await {
error!(backend = %backend_key, error = %e, "H3 send_data failed"); error!(backend = %backend_key, error = %e, "H3 send_data failed");
return Ok(error_response(StatusCode::BAD_GATEWAY, "H3 body send failed")); return Ok(error_response(StatusCode::BAD_GATEWAY, "H3 body send failed"));
} }
@@ -2863,8 +2939,23 @@ impl HttpProxyService {
ResponseFilter::apply_headers(route, headers, None); ResponseFilter::apply_headers(route, headers, None);
} }
// Stream response body back via an adapter // Stream response body back via unfold — correctly preserves waker across polls
let h3_body = H3ClientResponseBody { stream }; let body_stream = futures::stream::unfold(stream, |mut s| async move {
match s.recv_data().await {
Ok(Some(mut buf)) => {
use bytes::Buf;
let data = buf.copy_to_bytes(buf.remaining());
Some((Ok::<_, hyper::Error>(http_body::Frame::data(data)), s))
}
Ok(None) => None,
Err(e) => {
warn!("H3 response body recv error: {}", e);
None
}
}
});
let h3_body = http_body_util::StreamBody::new(body_stream);
let counting_body = CountingBody::new( let counting_body = CountingBody::new(
h3_body, h3_body,
Arc::clone(&self.metrics), Arc::clone(&self.metrics),
@@ -2881,10 +2972,16 @@ impl HttpProxyService {
let body: BoxBody<Bytes, hyper::Error> = BoxBody::new(counting_body); let body: BoxBody<Bytes, hyper::Error> = BoxBody::new(counting_body);
// Register connection in pool on success // Register connection in pool on success (fresh connections only)
if status != StatusCode::BAD_GATEWAY { if status != StatusCode::BAD_GATEWAY {
let g = self.connection_pool.register_h3(pool_key.clone(), quic_conn); if let Some(gh) = gen_holder {
gen_holder.store(g, std::sync::atomic::Ordering::Relaxed); let g = self.connection_pool.register_h3(
pool_key.clone(),
quic_conn,
send_request,
);
gh.store(g, std::sync::atomic::Ordering::Relaxed);
}
} }
self.metrics.set_backend_protocol(backend_key, "h3"); self.metrics.set_backend_protocol(backend_key, "h3");
@@ -2913,41 +3010,6 @@ fn parse_alt_svc_h3_port(header_value: &str) -> Option<u16> {
None None
} }
/// Response body adapter for H3 client responses.
/// Reads data from the h3 `RequestStream` recv side and presents it as an `http_body::Body`.
struct H3ClientResponseBody {
stream: h3::client::RequestStream<h3_quinn::BidiStream<Bytes>, Bytes>,
}
impl http_body::Body for H3ClientResponseBody {
type Data = Bytes;
type Error = hyper::Error;
fn poll_frame(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Option<Result<http_body::Frame<Self::Data>, Self::Error>>> {
// h3's recv_data is async, so we need to poll it manually.
// Use a small future to poll the recv_data call.
use std::future::Future;
let mut fut = Box::pin(self.stream.recv_data());
match fut.as_mut().poll(_cx) {
Poll::Ready(Ok(Some(mut buf))) => {
use bytes::Buf;
let data = Bytes::copy_from_slice(buf.chunk());
buf.advance(buf.remaining());
Poll::Ready(Some(Ok(http_body::Frame::data(data))))
}
Poll::Ready(Ok(None)) => Poll::Ready(None),
Poll::Ready(Err(e)) => {
warn!("H3 response body recv error: {}", e);
Poll::Ready(None)
}
Poll::Pending => Poll::Pending,
}
}
}
/// Insecure certificate verifier for backend TLS connections (fallback only). /// Insecure certificate verifier for backend TLS connections (fallback only).
/// The production path uses the shared config from tls_handler which has the same /// The production path uses the shared config from tls_handler which has the same
/// behavior but with session resumption across all outbound connections. /// behavior but with session resumption across all outbound connections.
@@ -3016,6 +3078,7 @@ impl Default for HttpProxyService {
connection_pool: Arc::new(crate::connection_pool::ConnectionPool::new()), connection_pool: Arc::new(crate::connection_pool::ConnectionPool::new()),
protocol_cache: Arc::new(crate::protocol_cache::ProtocolCache::new()), protocol_cache: Arc::new(crate::protocol_cache::ProtocolCache::new()),
http_idle_timeout: DEFAULT_HTTP_IDLE_TIMEOUT, http_idle_timeout: DEFAULT_HTTP_IDLE_TIMEOUT,
http_max_lifetime: DEFAULT_HTTP_MAX_LIFETIME,
ws_inactivity_timeout: DEFAULT_WS_INACTIVITY_TIMEOUT, ws_inactivity_timeout: DEFAULT_WS_INACTIVITY_TIMEOUT,
ws_max_lifetime: DEFAULT_WS_MAX_LIFETIME, ws_max_lifetime: DEFAULT_WS_MAX_LIFETIME,
quinn_client_endpoint: Arc::new(Self::create_quinn_client_endpoint()), quinn_client_endpoint: Arc::new(Self::create_quinn_client_endpoint()),

View File

@@ -89,6 +89,8 @@ pub struct ProtocolCacheEntryMetric {
pub protocol: String, pub protocol: String,
pub h3_port: Option<u16>, pub h3_port: Option<u16>,
pub age_secs: u64, pub age_secs: u64,
pub last_accessed_secs: u64,
pub last_probed_secs: u64,
pub h2_suppressed: bool, pub h2_suppressed: bool,
pub h3_suppressed: bool, pub h3_suppressed: bool,
pub h2_cooldown_remaining_secs: Option<u64>, pub h2_cooldown_remaining_secs: Option<u64>,
@@ -622,6 +624,24 @@ impl MetricsCollector {
self.ip_pending_tp.retain(|k, _| self.ip_connections.contains_key(k)); self.ip_pending_tp.retain(|k, _| self.ip_connections.contains_key(k));
self.ip_throughput.retain(|k, _| self.ip_connections.contains_key(k)); self.ip_throughput.retain(|k, _| self.ip_connections.contains_key(k));
self.ip_total_connections.retain(|k, _| self.ip_connections.contains_key(k)); self.ip_total_connections.retain(|k, _| self.ip_connections.contains_key(k));
// Safety-net: prune orphaned backend error/stats entries for backends
// that have no active or total connections (error-only backends).
// These accumulate when backend_connect_error/backend_handshake_error
// create entries but backend_connection_opened is never called.
let known_backends: HashSet<String> = self.backend_active.iter()
.map(|e| e.key().clone())
.chain(self.backend_total.iter().map(|e| e.key().clone()))
.collect();
self.backend_connect_errors.retain(|k, _| known_backends.contains(k));
self.backend_handshake_errors.retain(|k, _| known_backends.contains(k));
self.backend_request_errors.retain(|k, _| known_backends.contains(k));
self.backend_connect_time_us.retain(|k, _| known_backends.contains(k));
self.backend_connect_count.retain(|k, _| known_backends.contains(k));
self.backend_pool_hits.retain(|k, _| known_backends.contains(k));
self.backend_pool_misses.retain(|k, _| known_backends.contains(k));
self.backend_h2_failures.retain(|k, _| known_backends.contains(k));
self.backend_protocol.retain(|k, _| known_backends.contains(k));
} }
/// Remove per-route metrics for route IDs that are no longer active. /// Remove per-route metrics for route IDs that are no longer active.

View File

@@ -10,7 +10,6 @@ pub mod forwarder;
pub mod proxy_protocol; pub mod proxy_protocol;
pub mod tls_handler; pub mod tls_handler;
pub mod connection_tracker; pub mod connection_tracker;
pub mod socket_relay;
pub mod socket_opts; pub mod socket_opts;
pub mod udp_session; pub mod udp_session;
pub mod udp_listener; pub mod udp_listener;
@@ -22,7 +21,6 @@ pub use forwarder::*;
pub use proxy_protocol::*; pub use proxy_protocol::*;
pub use tls_handler::*; pub use tls_handler::*;
pub use connection_tracker::*; pub use connection_tracker::*;
pub use socket_relay::*;
pub use socket_opts::*; pub use socket_opts::*;
pub use udp_session::*; pub use udp_session::*;
pub use udp_listener::*; pub use udp_listener::*;

View File

@@ -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))
} }

View File

@@ -1,126 +1,4 @@
//! Socket handler relay for connecting client connections to a TypeScript handler //! Socket handler relay module.
//! via a Unix domain socket.
//! //!
//! Protocol: Send a JSON metadata line terminated by `\n`, then bidirectional relay. //! Note: The actual relay logic lives in `tcp_listener::relay_to_socket_handler()`
//! which has proper timeouts, cancellation, and metrics integration.
use tokio::net::UnixStream;
use tokio::io::{AsyncWriteExt, AsyncReadExt};
use tokio::net::TcpStream;
use serde::Serialize;
use tracing::debug;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct RelayMetadata {
connection_id: u64,
remote_ip: String,
remote_port: u16,
local_port: u16,
sni: Option<String>,
route_name: String,
initial_data_base64: Option<String>,
}
/// Relay a client connection to a TypeScript handler via Unix domain socket.
///
/// Protocol: Send a JSON metadata line terminated by `\n`, then bidirectional relay.
pub async fn relay_to_handler(
client: TcpStream,
relay_socket_path: &str,
connection_id: u64,
remote_ip: String,
remote_port: u16,
local_port: u16,
sni: Option<String>,
route_name: String,
initial_data: Option<&[u8]>,
) -> std::io::Result<()> {
debug!(
"Relaying connection {} to handler socket {}",
connection_id, relay_socket_path
);
// Connect to TypeScript handler Unix socket
let mut handler = UnixStream::connect(relay_socket_path).await?;
// Build and send metadata header
let initial_data_base64 = initial_data.map(base64_encode);
let metadata = RelayMetadata {
connection_id,
remote_ip,
remote_port,
local_port,
sni,
route_name,
initial_data_base64,
};
let metadata_json = serde_json::to_string(&metadata)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
handler.write_all(metadata_json.as_bytes()).await?;
handler.write_all(b"\n").await?;
// Bidirectional relay between client and handler
let (mut client_read, mut client_write) = client.into_split();
let (mut handler_read, mut handler_write) = handler.into_split();
let c2h = tokio::spawn(async move {
let mut buf = vec![0u8; 65536];
loop {
let n = match client_read.read(&mut buf).await {
Ok(0) | Err(_) => break,
Ok(n) => n,
};
if handler_write.write_all(&buf[..n]).await.is_err() {
break;
}
}
let _ = handler_write.shutdown().await;
});
let h2c = tokio::spawn(async move {
let mut buf = vec![0u8; 65536];
loop {
let n = match handler_read.read(&mut buf).await {
Ok(0) | Err(_) => break,
Ok(n) => n,
};
if client_write.write_all(&buf[..n]).await.is_err() {
break;
}
}
let _ = client_write.shutdown().await;
});
let _ = tokio::join!(c2h, h2c);
debug!("Relay connection {} completed", connection_id);
Ok(())
}
/// Simple base64 encoding without external dependency.
fn base64_encode(data: &[u8]) -> String {
const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut result = String::new();
for chunk in data.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
let n = (b0 << 16) | (b1 << 8) | b2;
result.push(CHARS[((n >> 18) & 0x3F) as usize] as char);
result.push(CHARS[((n >> 12) & 0x3F) as usize] as char);
if chunk.len() > 1 {
result.push(CHARS[((n >> 6) & 0x3F) as usize] as char);
} else {
result.push('=');
}
if chunk.len() > 2 {
result.push(CHARS[(n & 0x3F) as usize] as char);
} else {
result.push('=');
}
}
result
}

View File

@@ -182,6 +182,7 @@ impl TcpListenerManager {
http_proxy_svc.set_backend_tls_config_alpn(tls_handler::shared_backend_tls_config_alpn()); http_proxy_svc.set_backend_tls_config_alpn(tls_handler::shared_backend_tls_config_alpn());
http_proxy_svc.set_connection_timeouts( http_proxy_svc.set_connection_timeouts(
std::time::Duration::from_millis(conn_config.socket_timeout_ms), std::time::Duration::from_millis(conn_config.socket_timeout_ms),
std::time::Duration::from_millis(conn_config.max_connection_lifetime_ms),
std::time::Duration::from_millis(conn_config.socket_timeout_ms), std::time::Duration::from_millis(conn_config.socket_timeout_ms),
std::time::Duration::from_millis(conn_config.max_connection_lifetime_ms), std::time::Duration::from_millis(conn_config.max_connection_lifetime_ms),
); );
@@ -220,6 +221,7 @@ impl TcpListenerManager {
http_proxy_svc.set_backend_tls_config_alpn(tls_handler::shared_backend_tls_config_alpn()); http_proxy_svc.set_backend_tls_config_alpn(tls_handler::shared_backend_tls_config_alpn());
http_proxy_svc.set_connection_timeouts( http_proxy_svc.set_connection_timeouts(
std::time::Duration::from_millis(conn_config.socket_timeout_ms), std::time::Duration::from_millis(conn_config.socket_timeout_ms),
std::time::Duration::from_millis(conn_config.max_connection_lifetime_ms),
std::time::Duration::from_millis(conn_config.socket_timeout_ms), std::time::Duration::from_millis(conn_config.socket_timeout_ms),
std::time::Duration::from_millis(conn_config.max_connection_lifetime_ms), std::time::Duration::from_millis(conn_config.max_connection_lifetime_ms),
); );
@@ -263,6 +265,7 @@ impl TcpListenerManager {
http_proxy_svc.set_backend_tls_config_alpn(tls_handler::shared_backend_tls_config_alpn()); http_proxy_svc.set_backend_tls_config_alpn(tls_handler::shared_backend_tls_config_alpn());
http_proxy_svc.set_connection_timeouts( http_proxy_svc.set_connection_timeouts(
std::time::Duration::from_millis(config.socket_timeout_ms), std::time::Duration::from_millis(config.socket_timeout_ms),
std::time::Duration::from_millis(config.max_connection_lifetime_ms),
std::time::Duration::from_millis(config.socket_timeout_ms), std::time::Duration::from_millis(config.socket_timeout_ms),
std::time::Duration::from_millis(config.max_connection_lifetime_ms), std::time::Duration::from_millis(config.max_connection_lifetime_ms),
); );

View File

@@ -363,6 +363,7 @@ impl RustProxy {
// Start the throughput sampling task with cooperative cancellation // Start the throughput sampling task with cooperative cancellation
let metrics = Arc::clone(&self.metrics); let metrics = Arc::clone(&self.metrics);
let conn_tracker = self.listener_manager.as_ref().unwrap().conn_tracker().clone(); let conn_tracker = self.listener_manager.as_ref().unwrap().conn_tracker().clone();
let http_proxy = self.listener_manager.as_ref().unwrap().http_proxy().clone();
let interval_ms = self.options.metrics.as_ref() let interval_ms = self.options.metrics.as_ref()
.and_then(|m| m.sample_interval_ms) .and_then(|m| m.sample_interval_ms)
.unwrap_or(1000); .unwrap_or(1000);
@@ -378,6 +379,9 @@ impl RustProxy {
metrics.sample_all(); metrics.sample_all();
// Periodically clean up stale rate-limit timestamp entries // Periodically clean up stale rate-limit timestamp entries
conn_tracker.cleanup_stale_timestamps(); conn_tracker.cleanup_stale_timestamps();
// Clean up expired rate limiter entries to prevent unbounded
// growth from unique IPs after traffic stops
http_proxy.cleanup_all_rate_limiters();
} }
} }
} }
@@ -950,6 +954,8 @@ impl RustProxy {
protocol: e.protocol, protocol: e.protocol,
h3_port: e.h3_port, h3_port: e.h3_port,
age_secs: e.age_secs, age_secs: e.age_secs,
last_accessed_secs: e.last_accessed_secs,
last_probed_secs: e.last_probed_secs,
h2_suppressed: e.h2_suppressed, h2_suppressed: e.h2_suppressed,
h3_suppressed: e.h3_suppressed, h3_suppressed: e.h3_suppressed,
h2_cooldown_remaining_secs: e.h2_cooldown_remaining_secs, h2_cooldown_remaining_secs: e.h2_cooldown_remaining_secs,

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@push.rocks/smartproxy', name: '@push.rocks/smartproxy',
version: '26.1.0', version: '26.2.4',
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.'
} }

View File

@@ -26,6 +26,8 @@ interface IDatagramRelayMessage {
* - TS→Rust: { type: "reply", sourceIp, sourcePort, destPort, payloadBase64 } * - TS→Rust: { type: "reply", sourceIp, sourcePort, destPort, payloadBase64 }
*/ */
export class DatagramHandlerServer { export class DatagramHandlerServer {
private static readonly MAX_BUFFER_SIZE = 50 * 1024 * 1024; // 50 MB
private server: plugins.net.Server | null = null; private server: plugins.net.Server | null = null;
private connection: plugins.net.Socket | null = null; private connection: plugins.net.Socket | null = null;
private socketPath: string; private socketPath: string;
@@ -100,6 +102,11 @@ export class DatagramHandlerServer {
socket.on('data', (chunk: Buffer) => { socket.on('data', (chunk: Buffer) => {
this.readBuffer = Buffer.concat([this.readBuffer, chunk]); this.readBuffer = Buffer.concat([this.readBuffer, chunk]);
if (this.readBuffer.length > DatagramHandlerServer.MAX_BUFFER_SIZE) {
logger.log('error', `DatagramHandlerServer: buffer exceeded ${DatagramHandlerServer.MAX_BUFFER_SIZE} bytes, resetting`);
this.readBuffer = Buffer.alloc(0);
return;
}
this.processFrames(); this.processFrames();
}); });

View File

@@ -126,6 +126,8 @@ export interface IProtocolCacheEntry {
protocol: string; protocol: string;
h3Port: number | null; h3Port: number | null;
ageSecs: number; ageSecs: number;
lastAccessedSecs: number;
lastProbedSecs: number;
h2Suppressed: boolean; h2Suppressed: boolean;
h3Suppressed: boolean; h3Suppressed: boolean;
h2CooldownRemainingSecs: number | null; h2CooldownRemainingSecs: number | null;

View File

@@ -128,6 +128,7 @@ export class SmartProxy extends plugins.EventEmitter {
} }
// Handle unexpected exit (only emits error if not intentionally stopping) // Handle unexpected exit (only emits error if not intentionally stopping)
this.bridge.removeAllListeners('exit');
this.bridge.on('exit', (code: number | null, signal: string | null) => { this.bridge.on('exit', (code: number | null, signal: string | null) => {
if (this.stopping) return; if (this.stopping) return;
logger.log('error', `RustProxy exited unexpectedly (code=${code}, signal=${signal})`, { component: 'smart-proxy' }); logger.log('error', `RustProxy exited unexpectedly (code=${code}, signal=${signal})`, { component: 'smart-proxy' });

View File

@@ -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
}); });
} }

View File

@@ -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}`
}); });
} }

View File

@@ -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"