mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-11 21:12:09 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62666b5e27 | ||
|
|
2db9da5d06 | ||
|
|
371ca6e907 | ||
|
|
b02fb26c0c | ||
|
|
a2d875cebd |
@@ -5,10 +5,10 @@
|
||||
"version": "v9.0.0",
|
||||
"sha": "3a2844b7e9c422d3c10d287c895573f7108da1b3"
|
||||
},
|
||||
"github/gh-aw-actions/setup@v1.2.2": {
|
||||
"github/gh-aw-actions/setup@v1.0.1": {
|
||||
"repo": "github/gh-aw-actions/setup",
|
||||
"version": "v1.2.2",
|
||||
"sha": "53d0749e19424a918acd82b4d140264ce48f2f14"
|
||||
"version": "v1.0.1",
|
||||
"sha": "221ba4f4319de6b8e809d1a15528b3ae6a818aa5"
|
||||
}
|
||||
},
|
||||
"containers": {
|
||||
|
||||
@@ -191,9 +191,7 @@ jobs:
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: Release ${{ env.TAG_CHANNEL }} Version
|
||||
run: |
|
||||
pnpm release-version autobuild-latest
|
||||
pnpm polyfills:generate
|
||||
run: pnpm release-version autobuild-latest
|
||||
|
||||
- name: Tauri build for Windows-macOS-Linux
|
||||
uses: tauri-apps/tauri-action@v1
|
||||
@@ -273,9 +271,7 @@ jobs:
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: Release ${{ env.TAG_CHANNEL }} Version
|
||||
run: |
|
||||
pnpm release-version autobuild-latest
|
||||
pnpm polyfills:generate
|
||||
run: pnpm release-version autobuild-latest
|
||||
|
||||
- name: 'Setup for linux'
|
||||
run: |-
|
||||
@@ -408,9 +404,7 @@ jobs:
|
||||
pnpm run prebuild ${{ matrix.target }}
|
||||
|
||||
- name: Release ${{ env.TAG_CHANNEL }} Version
|
||||
run: |
|
||||
pnpm release-version autobuild-latest
|
||||
pnpm polyfills:generate
|
||||
run: pnpm release-version autobuild-latest
|
||||
|
||||
- name: Download WebView2 Runtime
|
||||
run: |
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install gh-aw extension
|
||||
uses: github/gh-aw-actions/setup-cli@53d0749e19424a918acd82b4d140264ce48f2f14 # v1.2.2
|
||||
uses: github/gh-aw-actions/setup-cli@406c7719e31d7fe354224d4bb8fe3d4aeb47da1c # v1.2.1
|
||||
with:
|
||||
version: v1.0.1
|
||||
- name: Install Rust Stable
|
||||
|
||||
@@ -115,9 +115,7 @@ jobs:
|
||||
|
||||
- name: Release ${{ env.TAG_CHANNEL }} Version
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
run: |
|
||||
pnpm release-version ${{ env.TAG_NAME }}
|
||||
pnpm polyfills:generate
|
||||
run: pnpm release-version ${{ env.TAG_NAME }}
|
||||
|
||||
- name: Add Rust Target
|
||||
if: github.event.inputs[matrix.input] == 'true'
|
||||
|
||||
@@ -58,9 +58,6 @@ jobs:
|
||||
- name: Run TypeScript typecheck
|
||||
run: pnpm typecheck
|
||||
|
||||
- name: Verify modern polyfill inventory against automatic discovery
|
||||
run: pnpm polyfills:check
|
||||
|
||||
- name: Run tests
|
||||
run: pnpm test
|
||||
|
||||
|
||||
+101
-280
File diff suppressed because one or more lines are too long
@@ -33,7 +33,6 @@ max-daily-ai-credits: 5000
|
||||
permissions:
|
||||
issues: read
|
||||
pull-requests: read
|
||||
copilot-requests: none
|
||||
|
||||
pre-agent-steps:
|
||||
- name: Make Copilot quota exhaustion non-blocking
|
||||
|
||||
Generated
+107
-288
File diff suppressed because one or more lines are too long
@@ -30,7 +30,6 @@ permissions:
|
||||
contents: read
|
||||
issues: read
|
||||
pull-requests: read
|
||||
copilot-requests: none
|
||||
steps:
|
||||
- name: Load review rules from the base revision
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
|
||||
+101
-280
File diff suppressed because one or more lines are too long
@@ -28,7 +28,6 @@ permissions:
|
||||
contents: read
|
||||
issues: read
|
||||
pull-requests: read
|
||||
copilot-requests: none
|
||||
max-ai-credits: 60
|
||||
max-turns: 12
|
||||
max-daily-ai-credits: 500
|
||||
|
||||
@@ -101,17 +101,6 @@ For frontend-only development, use `pnpm web:dev`. It does not start the Rust ba
|
||||
|
||||
### Build the Project
|
||||
|
||||
`pnpm web:build` uses a generated modern-polyfill inventory and rejects changed
|
||||
emitted module inputs, output, targets, or locked dependencies before writing
|
||||
output. Inputs include dependency code and code later removed by minification.
|
||||
When it reports a stale inventory, run `pnpm polyfills:generate` and commit
|
||||
`scripts/modern-polyfills.json`. Generation preserves automatic discovery's import
|
||||
order and requires byte-identical automatic/explicit outputs for the macOS, Linux,
|
||||
and Windows production defines. CI repeats this comparison with
|
||||
`pnpm polyfills:check`; browser targets and worker configuration stay unchanged.
|
||||
Release build jobs regenerate after rewriting versions because version data is
|
||||
also emitted in frontend modules.
|
||||
|
||||
Standard build:
|
||||
|
||||
```bash
|
||||
|
||||
Generated
+4
-4
@@ -159,9 +159,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "1.9.5"
|
||||
version = "1.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84a22b5d51209f6de83d37a0b33d320acb8da88823e42a94c075140708f032a5"
|
||||
checksum = "db4c94f57a570fce5d381be3289b3f79aef6332d72948fe82ab5772e423ccf92"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
@@ -10126,9 +10126,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "9.5.0"
|
||||
version = "9.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0700e6c427aff64acdbf02e85fbfd946c2ee12319d1af013092f3a2178914ff8"
|
||||
checksum = "df3845f734baabdd02de353a20580e30ceccd03897def44a4eb8e9654992e294"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"bzip2",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
- 修复全局模式默认停在 `DIRECT` 或 `REJECT` 导致流量未经代理,现自动切换到第一个策略组
|
||||
- 修复订阅中未加引号的 REALITY `short-id`(如 `1234e123`)被误读为数字,导致订阅无法启用
|
||||
- 修复订阅中唯一的代理组名为 `GLOBAL` 或没有代理组时,规则模式下代理页面显示「未获取到代理信息」
|
||||
- 修复切换订阅后节点延迟显示旧订阅结果,进入代理页面时主动刷新延迟记录
|
||||
|
||||
**🍎 macOS**
|
||||
|
||||
|
||||
+2
-4
@@ -15,8 +15,6 @@
|
||||
"tauri": "tauri",
|
||||
"web:dev": "vite",
|
||||
"web:build": "tsrs -p . --noEmit && vite build",
|
||||
"polyfills:generate": "node scripts/generate-modern-polyfills.mjs --write",
|
||||
"polyfills:check": "node scripts/generate-modern-polyfills.mjs --check",
|
||||
"web:serve": "vite preview",
|
||||
"prebuild": "node scripts/prebuild.mjs",
|
||||
"updater": "node scripts/updater.mjs",
|
||||
@@ -69,7 +67,7 @@
|
||||
"dayjs": "1.11.23",
|
||||
"foxact": "^0.3.12",
|
||||
"foxts": "^5.9.1",
|
||||
"i18next": "^27.0.0",
|
||||
"i18next": "^26.4.2",
|
||||
"js-yaml": "^5.4.2",
|
||||
"lodash-es": "^4.18.1",
|
||||
"meta-json-schema": "^1.19.31",
|
||||
@@ -81,7 +79,7 @@
|
||||
"react-dom": "19.3.1",
|
||||
"react-error-boundary": "6.1.6",
|
||||
"react-hook-form": "^7.88.0",
|
||||
"react-i18next": "18.0.1",
|
||||
"react-i18next": "17.1.13",
|
||||
"react-markdown": "10.1.0",
|
||||
"react-router": "^8.4.0",
|
||||
"rehype-raw": "^7.0.0",
|
||||
|
||||
Generated
+21
-18
@@ -239,8 +239,8 @@ importers:
|
||||
specifier: ^5.9.1
|
||||
version: 5.9.1
|
||||
i18next:
|
||||
specifier: ^27.0.0
|
||||
version: 27.0.1(typescript@6.0.3)
|
||||
specifier: ^26.4.2
|
||||
version: 26.4.2(typescript@6.0.3)
|
||||
js-yaml:
|
||||
specifier: ^5.4.2
|
||||
version: 5.4.2
|
||||
@@ -275,8 +275,8 @@ importers:
|
||||
specifier: ^7.88.0
|
||||
version: 7.88.0(react@19.3.1)
|
||||
react-i18next:
|
||||
specifier: 18.0.1
|
||||
version: 18.0.1(@types/react@19.3.0)(i18next@27.0.1(typescript@6.0.3))(react@19.3.1)(typescript@6.0.3)
|
||||
specifier: 17.1.13
|
||||
version: 17.1.13(i18next@26.4.2(typescript@6.0.3))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(typescript@6.0.3)
|
||||
react-markdown:
|
||||
specifier: 10.1.0
|
||||
version: 10.1.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0)
|
||||
@@ -3689,9 +3689,8 @@ packages:
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
i18next@27.0.1:
|
||||
resolution: {integrity: sha512-KaE5ozP8UUeVpOJIwMVbbnpRKpyYQuxd9ZbseJoDzezE3nvLnZZQn20pkzyIJwPI2Uet/Qs3WzPTG812f6F/Eg==}
|
||||
engines: {node: '>=22'}
|
||||
i18next@26.4.2:
|
||||
resolution: {integrity: sha512-RX+R0VLg13IbvRuJSxnqykUFS9vQZTl8wYpWPCIUDWVrSGjsQywB5Y+pjzrkboxGAuYfJZVH1InFTdgBdxq6ug==}
|
||||
peerDependencies:
|
||||
typescript: ^5 || ^6 || ^7
|
||||
peerDependenciesMeta:
|
||||
@@ -4397,16 +4396,18 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17 || ^18 || ^19
|
||||
|
||||
react-i18next@18.0.1:
|
||||
resolution: {integrity: sha512-kPWXI0E/lrPj3+/umcKNzYZVnWmEedLiamMzDIHfbs+5ANf6yzvlZsjKou8f7kaGkfMMDLIZBejV+4aSN1CyBw==}
|
||||
engines: {node: '>=22'}
|
||||
react-i18next@17.1.13:
|
||||
resolution: {integrity: sha512-ZxhOSlyKSKPtbUNPLwYMmE3w3bhoc0jaZJZ6bc7wGOT6cLbrbs//6m01HxCLtFdSjHvFpwFwoPhxxzIlHSs9cw==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
i18next: '>= 27.0.1'
|
||||
react: '>= 18.0.0'
|
||||
i18next: '>= 26.2.0'
|
||||
react: '>= 16.8.0'
|
||||
react-dom: '*'
|
||||
react-native: '*'
|
||||
typescript: ^5 || ^6 || ^7
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
react-dom:
|
||||
optional: true
|
||||
react-native:
|
||||
optional: true
|
||||
typescript:
|
||||
optional: true
|
||||
@@ -8532,7 +8533,7 @@ snapshots:
|
||||
|
||||
husky@9.1.7: {}
|
||||
|
||||
i18next@27.0.1(typescript@6.0.3):
|
||||
i18next@26.4.2(typescript@6.0.3):
|
||||
optionalDependencies:
|
||||
typescript: 6.0.3
|
||||
|
||||
@@ -9356,13 +9357,15 @@ snapshots:
|
||||
dependencies:
|
||||
react: 19.3.1
|
||||
|
||||
react-i18next@18.0.1(@types/react@19.3.0)(i18next@27.0.1(typescript@6.0.3))(react@19.3.1)(typescript@6.0.3):
|
||||
react-i18next@17.1.13(i18next@26.4.2(typescript@6.0.3))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(typescript@6.0.3):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.29.7
|
||||
html-parse-stringify: 4.0.1
|
||||
i18next: 27.0.1(typescript@6.0.3)
|
||||
i18next: 26.4.2(typescript@6.0.3)
|
||||
react: 19.3.1
|
||||
use-sync-external-store: 1.7.0(react@19.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 19.3.0
|
||||
react-dom: 19.3.1(react@19.3.1)
|
||||
typescript: 6.0.3
|
||||
|
||||
react-is@16.13.1: {}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
|
||||
import { build } from 'vite'
|
||||
|
||||
const [command, ...extra] = process.argv.slice(2)
|
||||
if (!['--write', '--check'].includes(command) || extra.length) {
|
||||
throw new Error(
|
||||
'Usage: node scripts/generate-modern-polyfills.mjs --write|--check',
|
||||
)
|
||||
}
|
||||
const inventoryPath = 'scripts/modern-polyfills.json'
|
||||
const staleMessage =
|
||||
'Modern polyfill inventory is stale; run pnpm polyfills:generate and commit scripts/modern-polyfills.json'
|
||||
|
||||
function outputHashes(result) {
|
||||
assert(
|
||||
!Array.isArray(result) && 'output' in result,
|
||||
'Expected one app output',
|
||||
)
|
||||
return result.output
|
||||
.map((file) => [
|
||||
file.fileName,
|
||||
createHash('sha256')
|
||||
.update(file.type === 'chunk' ? file.code : file.source)
|
||||
.digest('hex'),
|
||||
])
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
}
|
||||
|
||||
const temporary = mkdtempSync(path.join(os.tmpdir(), 'modern-polyfills-'))
|
||||
const capture = path.join(temporary, 'capture.json')
|
||||
const candidate = path.join(temporary, 'inventory.json')
|
||||
const snapshots = []
|
||||
const fingerprints = []
|
||||
let imports
|
||||
try {
|
||||
process.env.MODERN_POLYFILLS_CAPTURE = capture
|
||||
// OS_PLATFORM changes emitted code; certify each production define without
|
||||
// launching a native app or normalizing away code the detector actually sees.
|
||||
for (const platform of ['darwin', 'linux', 'win32']) {
|
||||
const config = {
|
||||
logLevel: 'error',
|
||||
define: { OS_PLATFORM: JSON.stringify(platform) },
|
||||
build: { write: false },
|
||||
}
|
||||
const automatic = await build(config)
|
||||
const detected = JSON.parse(readFileSync(capture, 'utf8'))
|
||||
if (imports) assert.deepEqual(detected.imports, imports)
|
||||
imports = detected.imports
|
||||
fingerprints.push(detected.fingerprint)
|
||||
snapshots.push({ config, hashes: outputHashes(automatic) })
|
||||
}
|
||||
const content = `${JSON.stringify({ imports, fingerprints: [...new Set(fingerprints)].sort() }, null, 2)}\n`
|
||||
writeFileSync(candidate, content)
|
||||
delete process.env.MODERN_POLYFILLS_CAPTURE
|
||||
process.env.MODERN_POLYFILLS_INVENTORY = candidate
|
||||
for (const { config, hashes } of snapshots) {
|
||||
assert.deepEqual(
|
||||
outputHashes(await build(config)),
|
||||
hashes,
|
||||
'Explicit polyfills must be byte-identical to automatic discovery',
|
||||
)
|
||||
}
|
||||
if (command === '--check') {
|
||||
assert.equal(readFileSync(inventoryPath, 'utf8'), content, staleMessage)
|
||||
} else {
|
||||
writeFileSync(inventoryPath, content)
|
||||
}
|
||||
console.log(
|
||||
`Modern polyfills ${command === '--check' ? 'verified' : 'generated'}: ${imports.length} imports; all three OS outputs byte-identical to automatic discovery`,
|
||||
)
|
||||
} finally {
|
||||
delete process.env.MODERN_POLYFILLS_CAPTURE
|
||||
delete process.env.MODERN_POLYFILLS_INVENTORY
|
||||
rmSync(temporary, { recursive: true })
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
{
|
||||
"imports": [
|
||||
"core-js/modules/es.error.cause.js",
|
||||
"core-js/modules/es.array.at.js",
|
||||
"core-js/modules/es.array.includes.js",
|
||||
"core-js/modules/es.array.push.js",
|
||||
"core-js/modules/es.array.unshift.js",
|
||||
"core-js/modules/es.array-buffer.detached.js",
|
||||
"core-js/modules/es.array-buffer.transfer.js",
|
||||
"core-js/modules/es.array-buffer.transfer-to-fixed-length.js",
|
||||
"core-js/modules/es.iterator.constructor.js",
|
||||
"core-js/modules/es.iterator.every.js",
|
||||
"core-js/modules/es.iterator.filter.js",
|
||||
"core-js/modules/es.iterator.find.js",
|
||||
"core-js/modules/es.iterator.flat-map.js",
|
||||
"core-js/modules/es.iterator.for-each.js",
|
||||
"core-js/modules/es.iterator.map.js",
|
||||
"core-js/modules/es.iterator.reduce.js",
|
||||
"core-js/modules/es.iterator.some.js",
|
||||
"core-js/modules/es.iterator.to-array.js",
|
||||
"core-js/modules/es.json.parse.js",
|
||||
"core-js/modules/es.json.stringify.js",
|
||||
"core-js/modules/es.map.get-or-insert.js",
|
||||
"core-js/modules/es.map.get-or-insert-computed.js",
|
||||
"core-js/modules/es.regexp.flags.js",
|
||||
"core-js/modules/es.set.difference.v2.js",
|
||||
"core-js/modules/es.set.intersection.v2.js",
|
||||
"core-js/modules/es.set.is-disjoint-from.v2.js",
|
||||
"core-js/modules/es.set.is-subset-of.v2.js",
|
||||
"core-js/modules/es.set.is-superset-of.v2.js",
|
||||
"core-js/modules/es.set.symmetric-difference.v2.js",
|
||||
"core-js/modules/es.set.union.v2.js",
|
||||
"core-js/modules/es.string.at-alternative.js",
|
||||
"core-js/modules/es.typed-array.at.js",
|
||||
"core-js/modules/es.typed-array.fill.js",
|
||||
"core-js/modules/es.typed-array.find-last.js",
|
||||
"core-js/modules/es.typed-array.find-last-index.js",
|
||||
"core-js/modules/es.typed-array.set.js",
|
||||
"core-js/modules/es.typed-array.sort.js",
|
||||
"core-js/modules/es.typed-array.to-reversed.js",
|
||||
"core-js/modules/es.typed-array.to-sorted.js",
|
||||
"core-js/modules/es.typed-array.with.js",
|
||||
"core-js/modules/es.uint8-array.set-from-base64.js",
|
||||
"core-js/modules/es.uint8-array.set-from-hex.js",
|
||||
"core-js/modules/es.uint8-array.to-base64.js",
|
||||
"core-js/modules/es.uint8-array.to-hex.js",
|
||||
"core-js/modules/es.weak-map.get-or-insert.js",
|
||||
"core-js/modules/es.weak-map.get-or-insert-computed.js",
|
||||
"core-js/modules/esnext.array.group.js",
|
||||
"core-js/modules/web.dom-exception.stack.js",
|
||||
"core-js/modules/web.immediate.js",
|
||||
"core-js/modules/web.url-search-params.delete.js",
|
||||
"core-js/modules/web.url-search-params.has.js",
|
||||
"core-js/modules/web.url-search-params.size.js",
|
||||
"core-js/modules/es.object.has-own.js",
|
||||
"core-js/modules/es.array.with.js",
|
||||
"core-js/modules/es.symbol.async-dispose.js",
|
||||
"core-js/modules/es.async-iterator.async-dispose.js",
|
||||
"core-js/modules/es.symbol.dispose.js",
|
||||
"core-js/modules/es.iterator.dispose.js",
|
||||
"core-js/modules/es.aggregate-error.cause.js",
|
||||
"core-js/modules/es.array.find-last.js",
|
||||
"core-js/modules/es.iterator.drop.js",
|
||||
"core-js/modules/es.iterator.take.js",
|
||||
"core-js/modules/web.structured-clone.js"
|
||||
],
|
||||
"fingerprints": [
|
||||
"30dd6a7f16e7065fa9803260e11aa2f2db56cabb69664257c940ab05bae8505b",
|
||||
"5630fe8a486687073b6d9894dd39735b77d5d8177432a4ada81947a429ff6991",
|
||||
"ddbb980f9ad3fe92ee7c30248d46ed4cdeb421c714266e627b825b59042c30bc"
|
||||
]
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import legacy, { detectPolyfills } from '@vitejs/plugin-legacy'
|
||||
|
||||
const digest = (value) => createHash('sha256').update(value).digest('hex')
|
||||
const inventoryPath = 'scripts/modern-polyfills.json'
|
||||
const staleMessage =
|
||||
'Modern polyfill inventory is stale; run pnpm polyfills:generate and commit scripts/modern-polyfills.json'
|
||||
|
||||
// Bind complete emitted module inputs as well as output: minification can hide APIs.
|
||||
// Generation double-checks the exported detector against legacy's automatic build.
|
||||
export function guardedLegacy(options) {
|
||||
const capturePath = process.env.MODERN_POLYFILLS_CAPTURE
|
||||
const inventory = capturePath
|
||||
? null
|
||||
: JSON.parse(
|
||||
readFileSync(
|
||||
process.env.MODERN_POLYFILLS_INVENTORY || inventoryPath,
|
||||
'utf8',
|
||||
),
|
||||
)
|
||||
const plugins = legacy({
|
||||
...options,
|
||||
modernPolyfills: Boolean(capturePath),
|
||||
additionalModernPolyfills: [
|
||||
...options.additionalModernPolyfills,
|
||||
...(capturePath ? [] : inventory.imports),
|
||||
],
|
||||
})
|
||||
const postProcess = plugins.find(
|
||||
(plugin) => plugin.name === 'vite:legacy-post-process',
|
||||
)
|
||||
if (!postProcess?.renderChunk) {
|
||||
throw new Error('plugin-legacy must provide its post-process render hook')
|
||||
}
|
||||
const original = postProcess.renderChunk
|
||||
const render = typeof original === 'function' ? original : original.handler
|
||||
const discovered = new Map()
|
||||
let chunkNames
|
||||
let context
|
||||
if (capturePath) {
|
||||
postProcess.renderChunk = async function (raw, chunk, output, meta) {
|
||||
chunkNames ??= Object.keys(meta.chunks)
|
||||
const imports = new Set()
|
||||
await detectPolyfills(
|
||||
raw,
|
||||
options.modernTargets,
|
||||
options.assumptions || {},
|
||||
imports,
|
||||
)
|
||||
discovered.set(chunk.fileName, imports)
|
||||
return render.call(this, raw, chunk, output, meta)
|
||||
}
|
||||
}
|
||||
return [
|
||||
...plugins,
|
||||
{
|
||||
name: 'guard-modern-polyfills',
|
||||
enforce: 'post',
|
||||
apply: 'build',
|
||||
configResolved(config) {
|
||||
if (config.build.ssr || options.renderLegacyChunks !== false) {
|
||||
throw new Error(
|
||||
'Modern polyfill guard requires a modern-only app build',
|
||||
)
|
||||
}
|
||||
// Custom import paths must not bind the inventory to a checkout location.
|
||||
context = [
|
||||
{
|
||||
...options,
|
||||
additionalModernPolyfills: options.additionalModernPolyfills.map(
|
||||
(file) =>
|
||||
path.relative(process.cwd(), file).split(path.sep).join('/'),
|
||||
),
|
||||
},
|
||||
config.build.target,
|
||||
config.mode,
|
||||
...[
|
||||
'package.json',
|
||||
'pnpm-lock.yaml',
|
||||
'vite.config.mts',
|
||||
'scripts/modern-polyfills.mjs',
|
||||
'scripts/generate-modern-polyfills.mjs',
|
||||
].map((file) => digest(readFileSync(file))),
|
||||
...[
|
||||
'.browserslistrc',
|
||||
'browserslist',
|
||||
'src/.browserslistrc',
|
||||
'src/browserslist',
|
||||
].map((file) =>
|
||||
existsSync(file) ? digest(readFileSync(file)) : null,
|
||||
),
|
||||
digest(
|
||||
readFileSync(new URL(import.meta.resolve('@vitejs/plugin-legacy'))),
|
||||
),
|
||||
]
|
||||
},
|
||||
generateBundle(_output, bundle) {
|
||||
const imports = capturePath
|
||||
? [
|
||||
...new Set(
|
||||
chunkNames.flatMap((name) => [...discovered.get(name)]),
|
||||
),
|
||||
]
|
||||
: inventory.imports
|
||||
const chunks = Object.values(bundle).filter(
|
||||
(file) => file.type === 'chunk',
|
||||
)
|
||||
const moduleIds = new Set(
|
||||
chunks.flatMap((chunk) => Object.keys(chunk.modules)),
|
||||
)
|
||||
const inputs = [...moduleIds].map((id) => {
|
||||
// Rolldown's runtime is generated, not a transformed input module.
|
||||
if (id === '\0rolldown/runtime.js') return null
|
||||
// Vite's HTML entry code embeds an absolute import to the main module.
|
||||
if (id.endsWith('.html')) return digest(readFileSync(id))
|
||||
const code = this.getModuleInfo(id)?.code
|
||||
if (code == null) this.error(`Missing emitted module input: ${id}`)
|
||||
return digest(code)
|
||||
})
|
||||
const fingerprint = digest(
|
||||
JSON.stringify([
|
||||
context,
|
||||
inputs.sort(),
|
||||
chunks.map((chunk) => digest(chunk.code)).sort(),
|
||||
imports,
|
||||
]),
|
||||
)
|
||||
if (capturePath) {
|
||||
writeFileSync(capturePath, JSON.stringify({ fingerprint, imports }))
|
||||
} else if (!inventory.fingerprints.includes(fingerprint)) {
|
||||
this.error(staleMessage)
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -77,7 +77,7 @@ tauri-plugin-dialog = "2.8.4"
|
||||
tauri-plugin-fs = "2.7.1"
|
||||
tauri-plugin-deep-link = "2.6.3"
|
||||
tauri-plugin-window-state = "2.5.0"
|
||||
zip = "9.5.0"
|
||||
zip = "9.4.2"
|
||||
flate2 = "1.1.11"
|
||||
reqwest_dav = "0.3.3"
|
||||
aes-gcm = { version = "0.11.1" }
|
||||
@@ -97,7 +97,7 @@ async-trait = "0.1.92"
|
||||
clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.7", version = "2.7.7", features = [
|
||||
"client",
|
||||
] }
|
||||
arc-swap = "1.9.5"
|
||||
arc-swap = "1.9.4"
|
||||
tokio-rustls = "0.26.6"
|
||||
rustls = { version = "0.23.46", features = ["ring"] }
|
||||
webpki-roots = "1.0.11"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useLockFn } from 'ahooks'
|
||||
import { useCallback, useEffect, useReducer } from 'react'
|
||||
import { useCallback, useSyncExternalStore } from 'react'
|
||||
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
import delayManager, { type DelayUpdate } from '@/services/delay'
|
||||
@@ -17,13 +17,6 @@ const PRESET_PROXY_NAMES = [
|
||||
'COMPATIBLE',
|
||||
]
|
||||
|
||||
const identity = (previous: DelayUpdate, next: DelayUpdate): DelayUpdate =>
|
||||
previous.delay === next.delay &&
|
||||
previous.elapsed === next.elapsed &&
|
||||
previous.updatedAt === next.updatedAt
|
||||
? previous
|
||||
: next
|
||||
|
||||
const INITIAL_DELAY: DelayUpdate = { delay: -1, updatedAt: 0 }
|
||||
|
||||
export interface UseProxyDelayState {
|
||||
@@ -42,56 +35,37 @@ export function useProxyDelayState(
|
||||
const details = memberDetails(member)
|
||||
const unresolved = member.kind === 'unresolved'
|
||||
const isPreset = unresolved || PRESET_PROXY_NAMES.includes(name)
|
||||
const [delayState, setDelayState] = useReducer(identity, INITIAL_DELAY)
|
||||
const { verge } = useVerge()
|
||||
const timeout = verge?.default_latency_timeout || 10000
|
||||
|
||||
useEffect(() => {
|
||||
if (isPreset) return
|
||||
delayManager.setListener(name, groupName, setDelayState)
|
||||
return () => {
|
||||
delayManager.removeListener(name, groupName)
|
||||
}
|
||||
}, [name, groupName, isPreset])
|
||||
const subscribe = useCallback(
|
||||
(onChange: () => void) =>
|
||||
isPreset ? () => {} : delayManager.addListener(name, groupName, onChange),
|
||||
[name, groupName, isPreset],
|
||||
)
|
||||
const read = useCallback(
|
||||
() =>
|
||||
unresolved ? undefined : delayManager.getDelayUpdate(name, groupName),
|
||||
[name, groupName, unresolved],
|
||||
)
|
||||
const cachedUpdate = useSyncExternalStore(subscribe, read, read)
|
||||
|
||||
const updateDelay = useCallback(() => {
|
||||
if (unresolved) {
|
||||
setDelayState(INITIAL_DELAY)
|
||||
return
|
||||
}
|
||||
const cachedUpdate = delayManager.getDelayUpdate(name, groupName)
|
||||
if (cachedUpdate) {
|
||||
setDelayState({ ...cachedUpdate })
|
||||
return
|
||||
}
|
||||
|
||||
const fallbackDelay = delayManager.getDelayFix(member, groupName)
|
||||
if (fallbackDelay === -1) {
|
||||
setDelayState({ delay: -1, updatedAt: 0 })
|
||||
return
|
||||
}
|
||||
|
||||
let updatedAt = 0
|
||||
const history = details?.history
|
||||
if (history && history.length > 0) {
|
||||
const lastRecord = history[history.length - 1]
|
||||
const parsed = Date.parse(lastRecord.time)
|
||||
if (!Number.isNaN(parsed)) {
|
||||
updatedAt = parsed
|
||||
}
|
||||
}
|
||||
|
||||
setDelayState({ delay: fallbackDelay, updatedAt })
|
||||
}, [details?.history, groupName, member, name, unresolved])
|
||||
|
||||
useEffect(() => {
|
||||
updateDelay()
|
||||
}, [updateDelay])
|
||||
const fallbackDelay = delayManager.getDelayFix(member, groupName)
|
||||
const history = details?.history
|
||||
const lastRecord = history?.[history.length - 1]
|
||||
const parsed = lastRecord ? Date.parse(lastRecord.time) : NaN
|
||||
const delayState =
|
||||
cachedUpdate ??
|
||||
(fallbackDelay === -1
|
||||
? INITIAL_DELAY
|
||||
: {
|
||||
delay: fallbackDelay,
|
||||
updatedAt: Number.isNaN(parsed) ? 0 : parsed,
|
||||
})
|
||||
|
||||
const onDelay = useLockFn(async () => {
|
||||
if (!isInteractableMember(member)) return
|
||||
setDelayState({ delay: -2, updatedAt: Date.now() })
|
||||
setDelayState(await delayManager.checkDelay(member, groupName, timeout))
|
||||
await delayManager.checkDelay(member, groupName, timeout)
|
||||
})
|
||||
|
||||
return {
|
||||
|
||||
@@ -49,7 +49,11 @@ const ProxyPage = () => {
|
||||
>(null)
|
||||
|
||||
const { clashConfig } = useClashConfigData()
|
||||
const { refreshClashConfig } = useAppRefreshers()
|
||||
const { refreshClashConfig, refreshProxy } = useAppRefreshers()
|
||||
|
||||
useEffect(() => {
|
||||
refreshProxy().catch((error) => showNotice.error(error))
|
||||
}, [refreshProxy])
|
||||
|
||||
const normalizedMode = clashConfig?.mode?.toLowerCase()
|
||||
const curMode = isMode(normalizedMode) ? normalizedMode : undefined
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo, useRef } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import {
|
||||
getBaseConfig,
|
||||
getRuleProviders,
|
||||
@@ -6,8 +6,10 @@ import {
|
||||
} from 'tauri-plugin-mihomo-api'
|
||||
|
||||
import { useClashInfo, useRuntimeConfig } from '@/hooks/use-clash'
|
||||
import { useProfiles } from '@/hooks/use-profiles'
|
||||
import { useVerge } from '@/hooks/use-verge'
|
||||
import { getProxyView, getSystemProxy } from '@/services/cmds'
|
||||
import delayManager from '@/services/delay'
|
||||
import { useQuery } from '@/services/query-client'
|
||||
import { useRunState } from '@/store/app-store-context'
|
||||
import { resolveDisplayedMixedPort } from '@/utils/mixed-port'
|
||||
@@ -53,6 +55,8 @@ export const AppDataProvider = ({
|
||||
const { verge } = useVerge()
|
||||
const { data: runtimeConfig } = useRuntimeConfig()
|
||||
const { clashInfo } = useClashInfo()
|
||||
const { profiles } = useProfiles()
|
||||
const currentProfile = profiles?.current
|
||||
|
||||
const {
|
||||
data: proxyView,
|
||||
@@ -102,6 +106,12 @@ export const AppDataProvider = ({
|
||||
const runningMode = runState?.mode
|
||||
|
||||
const refreshProxy = useStableFn(_refetchProxyView)
|
||||
useEffect(() => {
|
||||
if (currentProfile === undefined) return
|
||||
delayManager.setProfile(currentProfile)
|
||||
void refreshProxy()
|
||||
}, [currentProfile, refreshProxy])
|
||||
|
||||
const refreshClashConfig = useStableFn(_refetchClashConfig)
|
||||
const refreshRules = useStableFn(_refetchRules)
|
||||
const refreshSysproxy = useStableFn(_refetchSysproxy)
|
||||
|
||||
+66
-16
@@ -5,26 +5,32 @@ vi.mock('tauri-plugin-mihomo-api', () => ({
|
||||
healthcheckNodeInProvider: vi.fn(async () => ({ delay: 120 })),
|
||||
}))
|
||||
|
||||
import type { ResolvedProxyMember } from '@/types/proxy-view'
|
||||
import type { InteractableProxyMember } from '@/types/proxy-view'
|
||||
|
||||
import delayManager from './delay'
|
||||
|
||||
const node = (name: string) =>
|
||||
({
|
||||
kind: 'node',
|
||||
ref: { kind: 'node', name, recordId: `r:${name}` },
|
||||
node: {
|
||||
recordId: `r:${name}`,
|
||||
name,
|
||||
history: [],
|
||||
source: { kind: 'core', proxyName: name },
|
||||
},
|
||||
}) as unknown as ResolvedProxyMember
|
||||
|
||||
const flush = () => new Promise((resolve) => setTimeout(resolve, 0))
|
||||
const node = (
|
||||
name: string,
|
||||
): Extract<InteractableProxyMember, { kind: 'node' }> => ({
|
||||
kind: 'node',
|
||||
ref: { kind: 'node', name, recordId: `r:${name}` },
|
||||
node: {
|
||||
recordId: `r:${name}`,
|
||||
name,
|
||||
type: 'Shadowsocks',
|
||||
history: [],
|
||||
source: { kind: 'core', proxyName: name },
|
||||
udp: false,
|
||||
xudp: false,
|
||||
tfo: false,
|
||||
mptcp: false,
|
||||
smux: false,
|
||||
},
|
||||
})
|
||||
|
||||
let settles = 0
|
||||
let unsubscribe: () => void
|
||||
const flush = () => new Promise((resolve) => setTimeout(resolve, 0))
|
||||
|
||||
beforeEach(() => {
|
||||
settles = 0
|
||||
@@ -39,7 +45,7 @@ describe('group delay completion', () => {
|
||||
test('notifies once after a batch settles', async () => {
|
||||
const proxies = Array.from({ length: 6 }, (_, index) => node(`n${index}`))
|
||||
|
||||
await delayManager.checkListDelay(proxies as never, 'g', 5000, 2)
|
||||
await delayManager.checkListDelay(proxies, 'g', 5000, 2)
|
||||
await flush()
|
||||
|
||||
expect(settles).toBe(1)
|
||||
@@ -51,7 +57,7 @@ describe('group delay completion', () => {
|
||||
other += 1
|
||||
})
|
||||
|
||||
await delayManager.checkDelay(node('a') as never, 'g', 5000)
|
||||
await delayManager.checkDelay(node('a'), 'g', 5000)
|
||||
await flush()
|
||||
|
||||
expect(settles).toBe(1)
|
||||
@@ -59,3 +65,47 @@ describe('group delay completion', () => {
|
||||
stop()
|
||||
})
|
||||
})
|
||||
|
||||
describe('profile delay isolation', () => {
|
||||
test('notifies both shared-key occurrences and preserves the surviving subscription', async () => {
|
||||
const first = vi.fn()
|
||||
const second = vi.fn()
|
||||
const stopFirst = delayManager.addListener('a', 'g', first)
|
||||
const stopSecond = delayManager.addListener('a', 'g', second)
|
||||
try {
|
||||
delayManager.setDelay('a', 'g', -2)
|
||||
const pending = delayManager.getDelayUpdate('a', 'g')
|
||||
expect(delayManager.getDelayUpdate('a', 'g')).toBe(pending)
|
||||
await flush()
|
||||
stopFirst()
|
||||
delayManager.setDelay('a', 'g', 120)
|
||||
expect(delayManager.getDelayUpdate('a', 'g')).not.toBe(pending)
|
||||
await flush()
|
||||
expect(first.mock.calls.map(([update]) => update.delay)).toEqual([-2])
|
||||
expect(second.mock.calls.map(([update]) => update.delay)).toEqual([
|
||||
-2, 120,
|
||||
])
|
||||
} finally {
|
||||
stopFirst()
|
||||
stopSecond()
|
||||
}
|
||||
})
|
||||
|
||||
test('clears cached delays and fences an old batch across a profile switch', async () => {
|
||||
delayManager.setProfile('initial')
|
||||
delayManager.setDelay('cached', 'g', 900)
|
||||
const oldBatch = delayManager.checkListDelay(
|
||||
[node('a'), node('not-started')],
|
||||
'g',
|
||||
5000,
|
||||
1,
|
||||
)
|
||||
delayManager.setProfile('next')
|
||||
expect(delayManager.getDelayUpdate('cached', 'g')).toBeUndefined()
|
||||
delayManager.setDelay('a', 'g', 42)
|
||||
await oldBatch
|
||||
await flush()
|
||||
expect(delayManager.getDelay('a', 'g')).toBe(42)
|
||||
expect(delayManager.getDelayUpdate('not-started', 'g')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
+83
-33
@@ -29,10 +29,12 @@ export interface DelayUpdate {
|
||||
const CACHE_TTL = 30 * 60 * 1000
|
||||
|
||||
class DelayManager {
|
||||
private profile: string | null = null
|
||||
private generation = 0
|
||||
private cache = new Map<string, DelayUpdate>()
|
||||
private urlMap = new Map<string, string>()
|
||||
|
||||
private listenerMap = new Map<string, (update: DelayUpdate) => void>()
|
||||
private listenerMap = new Map<string, Set<(update: DelayUpdate) => void>>()
|
||||
|
||||
private groupListenerMap = new Map<string, Set<() => void>>()
|
||||
// Consumers compare snapshot identity; replace it only when the group settles.
|
||||
@@ -49,6 +51,35 @@ class DelayManager {
|
||||
private itemFlushScheduled = false
|
||||
private groupFlushScheduled = false
|
||||
|
||||
setProfile(profile: string | null) {
|
||||
if (this.profile === profile) return
|
||||
this.profile = profile
|
||||
this.generation += 1
|
||||
this.cache.clear()
|
||||
this.activeBatches.clear()
|
||||
this.pendingItemUpdates.clear()
|
||||
this.pendingGroupUpdates.clear()
|
||||
this.groupSnapshots.clear()
|
||||
this.groupSetSnapshots.clear()
|
||||
|
||||
// Reset before a refreshed proxy snapshot can publish newer history.
|
||||
for (const [key, listeners] of this.listenerMap) {
|
||||
for (const listener of [...listeners]) {
|
||||
try {
|
||||
listener({ delay: -1, updatedAt: 0 })
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[DelayManager] Failed to reset delay listener: ${key}`,
|
||||
error,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const group of this.groupListenerMap.keys()) {
|
||||
this.queueGroupNotification(group)
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleOnNextFrame(run: () => void): void {
|
||||
if (typeof window !== 'undefined') {
|
||||
if (typeof window.requestAnimationFrame === 'function') {
|
||||
@@ -74,17 +105,19 @@ class DelayManager {
|
||||
this.pendingItemUpdates = new Map()
|
||||
|
||||
updates.forEach((queue, key) => {
|
||||
const listener = this.listenerMap.get(key)
|
||||
if (!listener) return
|
||||
const listeners = this.listenerMap.get(key)
|
||||
if (!listeners) return
|
||||
|
||||
queue.forEach((update) => {
|
||||
try {
|
||||
listener(update)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[DelayManager] 通知节点延迟监听器失败: ${key}`,
|
||||
error,
|
||||
)
|
||||
for (const listener of [...listeners]) {
|
||||
try {
|
||||
listener(update)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[DelayManager] 通知节点延迟监听器失败: ${key}`,
|
||||
error,
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -170,18 +203,22 @@ class DelayManager {
|
||||
return url || 'http://cp.cloudflare.com/generate_204'
|
||||
}
|
||||
|
||||
setListener(
|
||||
addListener(
|
||||
name: string,
|
||||
group: string,
|
||||
listener: (update: DelayUpdate) => void,
|
||||
) {
|
||||
): () => void {
|
||||
const key = hashKey(name, group)
|
||||
this.listenerMap.set(key, listener)
|
||||
}
|
||||
const listeners = this.listenerMap.get(key) ?? new Set()
|
||||
listeners.add(listener)
|
||||
this.listenerMap.set(key, listeners)
|
||||
|
||||
removeListener(name: string, group: string) {
|
||||
const key = hashKey(name, group)
|
||||
this.listenerMap.delete(key)
|
||||
return () => {
|
||||
const current = this.listenerMap.get(key)
|
||||
if (!current) return
|
||||
current.delete(listener)
|
||||
if (current.size === 0) this.listenerMap.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
/** Multiple views may subscribe independently; notifications occur only after settle. */
|
||||
@@ -237,7 +274,8 @@ class DelayManager {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return { ...entry }
|
||||
// React's external-store reads must retain identity until the value changes.
|
||||
return entry
|
||||
}
|
||||
|
||||
getDelay(name: string, group: string) {
|
||||
@@ -276,19 +314,21 @@ class DelayManager {
|
||||
member: InteractableProxyMember,
|
||||
group: string,
|
||||
timeout: number,
|
||||
): Promise<DelayUpdate> {
|
||||
): Promise<void> {
|
||||
const generation = this.generation
|
||||
// Entry points own the pending mark; measureDelay only writes results.
|
||||
this.setDelay(member.ref.name, group, -2)
|
||||
const update = await this.measureDelay(member, group, timeout)
|
||||
this.queueGroupNotification(group)
|
||||
return update
|
||||
await this.measureDelay(member, group, timeout, generation)
|
||||
if (generation === this.generation) this.queueGroupNotification(group)
|
||||
}
|
||||
|
||||
private async measureDelay(
|
||||
member: InteractableProxyMember,
|
||||
group: string,
|
||||
timeout: number,
|
||||
): Promise<DelayUpdate> {
|
||||
generation: number,
|
||||
): Promise<void> {
|
||||
if (generation !== this.generation) return
|
||||
const name = member.ref.name
|
||||
const providerName =
|
||||
member.kind === 'node' ? providerNameOf(member.node) : undefined
|
||||
@@ -324,14 +364,18 @@ class DelayManager {
|
||||
const elapsed = elapsedTime
|
||||
debugLog(`[DelayManager] 延迟测试完成,代理: ${name}, 结果: ${delay}ms`)
|
||||
|
||||
return this.setDelay(name, group, delay, { elapsed })
|
||||
if (generation === this.generation) {
|
||||
this.setDelay(name, group, delay, { elapsed })
|
||||
}
|
||||
} catch (error) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
console.error(`[DelayManager] 延迟测试出错,代理: ${name}`, error)
|
||||
const delay = 1e6 // error
|
||||
const elapsed = Date.now() - startTime
|
||||
|
||||
return this.setDelay(name, group, delay, { elapsed })
|
||||
if (generation === this.generation) {
|
||||
this.setDelay(name, group, delay, { elapsed })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,6 +385,7 @@ class DelayManager {
|
||||
timeout: number,
|
||||
concurrency = 36,
|
||||
) {
|
||||
const generation = this.generation
|
||||
debugLog(
|
||||
`[DelayManager] 批量测试延迟开始,组: ${group}, 数量: ${proxies.length}, 并发数: ${concurrency}`,
|
||||
)
|
||||
@@ -354,6 +399,7 @@ class DelayManager {
|
||||
const startTime = Date.now()
|
||||
|
||||
const help = async (): Promise<void> => {
|
||||
if (generation !== this.generation) return
|
||||
const currMember = proxies[index++]
|
||||
if (!currMember) return
|
||||
const currName = currMember.ref.name
|
||||
@@ -367,13 +413,15 @@ class DelayManager {
|
||||
}
|
||||
|
||||
// Do not reorder a list around the pointer while batch results are arriving.
|
||||
await this.measureDelay(currMember, group, timeout)
|
||||
await this.measureDelay(currMember, group, timeout, generation)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[DelayManager] 批量测试单个代理出错,代理: ${currName}`,
|
||||
error,
|
||||
)
|
||||
this.setDelay(currName, group, 1e6)
|
||||
if (generation === this.generation) {
|
||||
this.setDelay(currName, group, 1e6)
|
||||
}
|
||||
}
|
||||
|
||||
return help()
|
||||
@@ -391,12 +439,14 @@ class DelayManager {
|
||||
await Promise.all(promiseList)
|
||||
} finally {
|
||||
// Always release the batch and notify; otherwise failures leave stale sort state.
|
||||
const remaining = (this.activeBatches.get(group) ?? 1) - 1
|
||||
if (remaining > 0) {
|
||||
this.activeBatches.set(group, remaining)
|
||||
} else {
|
||||
this.activeBatches.delete(group)
|
||||
this.queueGroupNotification(group)
|
||||
if (generation === this.generation) {
|
||||
const remaining = (this.activeBatches.get(group) ?? 1) - 1
|
||||
if (remaining > 0) {
|
||||
this.activeBatches.set(group, remaining)
|
||||
} else {
|
||||
this.activeBatches.delete(group)
|
||||
this.queueGroupNotification(group)
|
||||
}
|
||||
}
|
||||
}
|
||||
const totalTime = Date.now() - startTime
|
||||
|
||||
+6
-13
@@ -1,29 +1,22 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import legacy from '@vitejs/plugin-legacy'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { defineConfig } from 'vite'
|
||||
import svgr from 'vite-plugin-svgr'
|
||||
|
||||
import { guardedLegacy } from './scripts/modern-polyfills.mjs'
|
||||
|
||||
const svgPlugin = svgr()
|
||||
const svgLoad = svgPlugin.load
|
||||
if (!svgLoad) throw new Error('vite-plugin-svgr must provide a load hook')
|
||||
// SVGR's internal include filter still crosses the JS bridge for every module.
|
||||
svgPlugin.load = {
|
||||
filter: { id: /\.svg\?react$/ },
|
||||
handler: typeof svgLoad === 'function' ? svgLoad : svgLoad.handler,
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
root: 'src',
|
||||
server: { port: 3000 },
|
||||
plugins: [
|
||||
svgPlugin,
|
||||
svgr(),
|
||||
react(),
|
||||
guardedLegacy({
|
||||
legacy({
|
||||
modernTargets: ['edge>=109', 'safari>=14'],
|
||||
renderLegacyChunks: false,
|
||||
// Auto-detected from the emitted bundle, dependencies included,
|
||||
// so post-Safari-14 APIs never depend on extending a manual list.
|
||||
modernPolyfills: true,
|
||||
additionalModernPolyfills: [
|
||||
path.resolve('./src/polyfills/matchMedia.js'),
|
||||
path.resolve('./src/polyfills/WeakRef.js'),
|
||||
|
||||
Reference in New Issue
Block a user