diff --git a/src/server/services/liveTrafficService.ts b/src/server/services/liveTrafficService.ts index 9a8c24f..382f637 100644 --- a/src/server/services/liveTrafficService.ts +++ b/src/server/services/liveTrafficService.ts @@ -639,10 +639,17 @@ function defaultClientFactory(port: number): NativeTrafficClient { })); } -function delay(milliseconds: number) { +function delay(milliseconds: number, signal: AbortSignal) { return new Promise((resolve) => { - const timer = setTimeout(resolve, milliseconds); + const finish = () => { + clearTimeout(timer); + signal.removeEventListener('abort', finish); + resolve(); + }; + const timer = setTimeout(finish, milliseconds); timer.unref(); + signal.addEventListener('abort', finish, { once: true }); + if (signal.aborted) finish(); }); } @@ -660,6 +667,7 @@ export function createLiveTrafficService({ const ledger = createLiveTrafficLedger({ enabled, gateway, resolveOrigin }); if (!enabled && unavailableError) ledger.markUnavailable(unavailableError); let stopped = false; + const stopController = new AbortController(); let controller: AbortController | null = null; let running: Promise | null = null; let failedProjection: NativeTrafficProjectionBatch | null = null; @@ -742,7 +750,7 @@ export function createLiveTrafficService({ while (!stopped) { if (!isRuntimeRunning()) { ledger.markStopped(); - await delay(RETRY_MS); + await delay(RETRY_MS, stopController.signal); continue; } controller = new AbortController(); @@ -756,7 +764,7 @@ export function createLiveTrafficService({ } finally { controller = null; } - if (!stopped) await delay(RETRY_MS); + if (!stopped) await delay(RETRY_MS, stopController.signal); } }; @@ -767,6 +775,7 @@ export function createLiveTrafficService({ }, async stop() { stopped = true; + stopController.abort(); controller?.abort(); await running; }, diff --git a/src/shared/versions.ts b/src/shared/versions.ts index 4f05cda..6ae4e02 100644 --- a/src/shared/versions.ts +++ b/src/shared/versions.ts @@ -1,7 +1,7 @@ export const HARBOR_VERSIONS = Object.freeze({ - macClient: '0.34.0', - gatewayClient: '0.36.0', - gatewayBackend: '0.36.0', + macClient: '0.34.1', + gatewayClient: '0.36.1', + gatewayBackend: '0.36.1', }); export interface ParsedVersion { diff --git a/test/server/singbox-native-api.test.js b/test/server/singbox-native-api.test.js index 5fccbc8..c85a175 100644 --- a/test/server/singbox-native-api.test.js +++ b/test/server/singbox-native-api.test.js @@ -223,6 +223,29 @@ for (const incompatible of [ }); } +test('collector stop interrupts an active retry delay', async () => { + const service = createLiveTrafficService({ + port: 19091, + enabled: true, + isRuntimeRunning: () => true, + clientFactory: () => ({ + async getVersion() { return { version: '1.13.18', apiVersion: 4 }; }, + async getStartedAt() { throw new Error('must not start'); }, + subscribeConnections() { throw new Error('must not subscribe'); }, + subscribeStatus() { throw new Error('must not subscribe'); }, + }), + }); + + service.start(); + await waitFor(() => service.snapshot().source.state === 'incompatible'); + await new Promise((resolve) => setImmediate(resolve)); + const stoppedBeforeNextTurn = await Promise.race([ + service.stop().then(() => true), + new Promise((resolve) => setImmediate(() => resolve(false))), + ]); + assert.equal(stoppedBeforeNextTurn, true); +}); + test('collector reconnects when either RC5 stream ends and cancels its sibling', async () => { let factoryCalls = 0; let authorizationCalls = 0; diff --git a/test/web/style-boundaries.test.js b/test/web/style-boundaries.test.js index efda8eb..c038ff9 100644 --- a/test/web/style-boundaries.test.js +++ b/test/web/style-boundaries.test.js @@ -40,26 +40,26 @@ const expectedImports = [ const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex'); const acceptedLedger = { counts: { - cascadeEdges: 1126, + cascadeEdges: 1153, customProperties: 115, - declarations: 4676, + declarations: 4698, important: 0, keyframes: 55, media: 22, - rules: 1262, - variableReferences: 1214, + rules: 1267, + variableReferences: 1222, }, hashes: { - cascadeEdges: '1f3c75839bd37bb312b9aed2987ed61571e5148f8b23c8288424cb193c7a8dda', + cascadeEdges: 'b92a7365b40a48b7c1e9a026b08170c35519567132036a86049b37eebdcd5b76', customProperties: '81d1e70737ae5a8e4b20d4c1be1b24685975a404863444da4a8821be1f0d5097', - declarations: '616fc7cb72a9db509f6b41e1808118c2e2be7da6ce4c078dfa2eb4a963f0dbb1', + declarations: '0e994db1d4ac16eb91d1dd0d74027fa132bad1344cf114db3641d83262116d07', duplicateKeyframes: '4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945', duplicateSelectors: '8982145dba05b33bf1c93b2d54cfbe76305b276ff3c657aa020144016ada5848', keyframes: 'b9b0bf3fad92e69b93ec0c24f01da15e78bbe89a095597d64ce4f7ef5e272fdd', - ruleDeclarationSequences: '3a65866ea25506c6fd79b68bfbbdeddd495f6957dbd99dec6a814b816ba4cca2', - selectors: '56e1c67b35230649a3c69b2aea67c1929580700f1b4357ee05dfb11012d72655', - variableReferences: '71722a81fa16b3586ae9ee5890f79176729dfdf7fe58c4d4994755f8059aa034', - witnesses: '2272ed9b07e02edd232c33a971c4cc5171f6a612ccaa36418933f0fd6bb465c1', + ruleDeclarationSequences: '6226141f0ff66b775008149ccf7d7a55e952c3f5d75961e275f0509ee352a4d6', + selectors: '3a165769801a80e9bee5082728a58f38bb427a19ac2c764de8586a5831c9ec87', + variableReferences: 'bdae48e482697d7cff6fdc434860cdacf19c8e3cee24b35574e461c3cff73a51', + witnesses: 'aa6334ad87124e5c0dc56af0f8927523aad3002444395adb1c50c0b8a36601bb', }, }; @@ -212,7 +212,7 @@ test('client typography uses the shared semantic scale outside the token owner', test('accepted stylesheet has pinned declaration, selector, keyframe, variable, and cascade ledgers', () => { const witnesses = readStyleWitnesses(root); - assert.equal(witnesses.length, 1292); + assert.equal(witnesses.length, 1281); assert.equal(witnesses.filter((witness) => witness.unknown || witness.ancestorUnknown).length, 0); const ledger = createStyleLedger(readStyleSource(root), { witnesses }); assert.deepEqual(ledger.counts, acceptedLedger.counts); @@ -409,8 +409,8 @@ test('main owns one public stylesheet and the regrouped production CSS is determ assert.equal((main.match(/import ['"][^'"]+\.css['"]/g) || []).length, 1); const assets = fs.readdirSync(path.join(root, 'dist/assets')).filter((file) => file.endsWith('.css')); - assert.deepEqual(assets, ['index-tJmxnB8a.css']); + assert.deepEqual(assets, ['index-Djad1gX3.css']); const built = fs.readFileSync(path.join(root, 'dist/assets', assets[0])); - assert.equal(built.byteLength, 176543); - assert.equal(sha256(built), '38a283204c656164c38c17aa03f41c931544e05b5732248807d911b2645bf970'); + assert.equal(built.byteLength, 178147); + assert.equal(sha256(built), 'bc1e317d3da0840d6f88a764c52385d70b97ff0bcb51d3911059a34444c14b09'); });