@7n/rules 1.13.1 → 1.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/package.json +1 -1
- package/rules/graphql/tooling/docs/main.md +1 -1
- package/rules/graphql/tooling/main.mjs +4 -4
- package/rules/js/eslint/docs/fix-worker.md +6 -5
- package/rules/js/eslint/docs/main.md +1 -1
- package/rules/js/eslint/fix-worker.mjs +70 -30
- package/rules/js/eslint/main.mjs +8 -7
- package/rules/js-run/runtime/docs/main.md +1 -1
- package/rules/js-run/runtime/main.mjs +3 -3
- package/rules/k8s/manifests/main.mjs +6 -6
- package/rules/nginx-default-tpl/template/docs/main.md +1 -1
- package/rules/nginx-default-tpl/template/main.mjs +5 -5
- package/rules/tauri/tooling/docs/main.md +1 -1
- package/rules/tauri/tooling/main.mjs +1 -1
- package/scripts/lib/docs/ensure-tool.md +24 -22
- package/scripts/lib/docs/run-conftest-batch.md +5 -3
- package/scripts/lib/ensure-tool.mjs +125 -31
- package/scripts/lib/lint-surface/blocking-inventory.mjs +59 -0
- package/scripts/lib/lint-surface/docs/blocking-inventory.md +30 -0
- package/scripts/lib/lint-surface/docs/index.md +2 -0
- package/scripts/lib/lint-surface/docs/policy-lint-adapter.md +2 -3
- package/scripts/lib/lint-surface/docs/run-detectors.md +3 -1
- package/scripts/lib/lint-surface/docs/scheduler.md +39 -0
- package/scripts/lib/lint-surface/docs/types.md +2 -2
- package/scripts/lib/lint-surface/policy-lint-adapter.mjs +4 -3
- package/scripts/lib/lint-surface/run-detectors.mjs +142 -31
- package/scripts/lib/lint-surface/scheduler.mjs +96 -0
- package/scripts/lib/lint-surface/types.mjs +3 -0
- package/scripts/lib/run-conftest-batch.mjs +16 -11
- package/scripts/utils/docs/index.md +1 -0
- package/scripts/utils/docs/spawn-async.md +33 -0
- package/scripts/utils/spawn-async.mjs +112 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded two-lane concurrent scheduler для `detectAll()` (ADR 260716-1354). Активний лише
|
|
3
|
+
* коли `concurrency > 1` (деталі — `run-detectors.mjs`); за замовчуванням (`concurrency === 1`)
|
|
4
|
+
* `detectAll` лишається на повністю послідовному шляху, спостережувано ідентичному до-ADR
|
|
5
|
+
* поведінці — ця функція там навіть не викликається.
|
|
6
|
+
*
|
|
7
|
+
* Два лейни за `isSerial(item)`: **serial lane** — власний sequential runner (mutex за
|
|
8
|
+
* конструкцією, items ніколи не перекриваються самі з собою); **parallel lane** — bounded
|
|
9
|
+
* pool до `concurrency` слотів. Обидва лейни виконуються конкурентно один з одним — свідомий
|
|
10
|
+
* вибір: serial-lane item, коли реально виконує свій blocking `spawnSync`, все одно заморожує
|
|
11
|
+
* весь event loop (тобто "конкурентність" із parallel lane суто структурна, не робить
|
|
12
|
+
* serial-lane item швидшим), а parallel-lane пул отримує реальну вигоду від одночасного
|
|
13
|
+
* очікування кількох `spawnAsync`-викликів.
|
|
14
|
+
*
|
|
15
|
+
* Перший виняток від `runItem` зупиняє нові старти в обох лейнах, `controller.abort()`
|
|
16
|
+
* сигналізує вже запущеним async-детекторам, і функція чекає завершення всіх уже
|
|
17
|
+
* стартованих items (кожен `runOne` сам ловить власну помилку — жоден виклик не відхиляє
|
|
18
|
+
* зовнішній `Promise.all`) перед поверненням.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @template T, R
|
|
23
|
+
* @typedef {object} PlanItemOutcome
|
|
24
|
+
* @property {T} item вхідний item
|
|
25
|
+
* @property {R} [result] результат `runItem`, якщо завершився успішно
|
|
26
|
+
* @property {unknown} [error] помилка `runItem` (перша — стає `infraError`)
|
|
27
|
+
* @property {boolean} [aborted] true — item отримав `AbortError` уже ПІСЛЯ того, як інший item
|
|
28
|
+
* зупинив плановий прогін (очікуване скасування, не нова інфра-помилка)
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @template T, R
|
|
33
|
+
* @param {T[]} items вхідний план (в оригінальному порядку)
|
|
34
|
+
* @param {object} opts опції планування
|
|
35
|
+
* @param {number} opts.concurrency bounded pool розмір для parallel lane (мінімум 1)
|
|
36
|
+
* @param {(item: T) => boolean} opts.isSerial чи item належить serial lane
|
|
37
|
+
* @param {(item: T, signal: AbortSignal) => Promise<R>} opts.runItem виконує один item;
|
|
38
|
+
* кидання зупиняє планування нових items і абортить `signal`
|
|
39
|
+
* @returns {Promise<{ results: PlanItemOutcome<T, R>[], infraError: unknown|null }>}
|
|
40
|
+
* `results` — лише items, що реально стартували (в порядку завершення, не вхідному);
|
|
41
|
+
* `infraError` — перша помилка `runItem`, або `null`, якщо всі items завершились успішно
|
|
42
|
+
*/
|
|
43
|
+
export async function runPlanConcurrently(items, { concurrency, isSerial, runItem }) {
|
|
44
|
+
const controller = new AbortController()
|
|
45
|
+
const parallelItems = []
|
|
46
|
+
const serialItems = []
|
|
47
|
+
for (const item of items) (isSerial(item) ? serialItems : parallelItems).push(item)
|
|
48
|
+
|
|
49
|
+
/** @type {PlanItemOutcome<T, R>[]} */
|
|
50
|
+
const results = []
|
|
51
|
+
let infraError = null
|
|
52
|
+
let stopped = false
|
|
53
|
+
|
|
54
|
+
const runOne = async item => {
|
|
55
|
+
if (stopped) return
|
|
56
|
+
try {
|
|
57
|
+
const result = await runItem(item, controller.signal)
|
|
58
|
+
results.push({ item, result })
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (stopped && error?.name === 'AbortError') {
|
|
61
|
+
results.push({ item, aborted: true })
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
results.push({ item, error })
|
|
65
|
+
if (!stopped) {
|
|
66
|
+
stopped = true
|
|
67
|
+
infraError = error
|
|
68
|
+
controller.abort()
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const runSerialLane = async () => {
|
|
74
|
+
for (const item of serialItems) {
|
|
75
|
+
if (stopped) break
|
|
76
|
+
await runOne(item)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const runParallelLane = async () => {
|
|
81
|
+
let next = 0
|
|
82
|
+
const worker = async () => {
|
|
83
|
+
while (!stopped) {
|
|
84
|
+
const i = next++
|
|
85
|
+
if (i >= parallelItems.length) return
|
|
86
|
+
await runOne(parallelItems[i])
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const workerCount = Math.min(concurrency, parallelItems.length)
|
|
90
|
+
await Promise.all(Array.from({ length: workerCount }, worker))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
await Promise.all([runSerialLane(), runParallelLane()])
|
|
94
|
+
|
|
95
|
+
return { results, infraError }
|
|
96
|
+
}
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
* `undefined` означає whole-repo
|
|
22
22
|
* @property {boolean} [verbose] `--verbose` CLI-прапорець; concern-и із зовнішніми
|
|
23
23
|
* інструментами (напр. `ga/workflows`) звіряються з ним, щоб не засмічувати прогрес-бар `lint --full`
|
|
24
|
+
* @property {AbortSignal} [signal] сигнал скасування — лише у parallel lane `detectAll()`
|
|
25
|
+
* (`N_RULES_LINT_CONCURRENCY>1`, ADR 260716-1354); async-детектори (`spawnAsync`-based)
|
|
26
|
+
* прокидають його далі, щоб перерватись при infrastructure-помилці іншого concern-а
|
|
24
27
|
*/
|
|
25
28
|
|
|
26
29
|
/**
|
|
@@ -9,16 +9,20 @@
|
|
|
9
9
|
* ризик дрифту (типу `spec.config` vs `spec.default.config` у
|
|
10
10
|
* `health_check_policy.rego`, що ми ловили cross-check тестами).
|
|
11
11
|
*
|
|
12
|
-
* Hard-fail на відсутність `conftest` — через `
|
|
12
|
+
* Hard-fail на відсутність `conftest` — через `ensureToolAsync`, що спочатку
|
|
13
13
|
* намагається авто-встановити, і лише після невдачі кидає виняток.
|
|
14
|
+
*
|
|
15
|
+
* Async (`spawnAsync`, не `spawnSync`) — детектор не блокує event loop, тож може
|
|
16
|
+
* виконуватись у parallel lane `detectAll()` (ADR 260716-1354). Приймає опційний
|
|
17
|
+
* `signal`/`timeoutMs` — прокидаються в `spawnAsync`.
|
|
14
18
|
*/
|
|
15
|
-
import { spawnSync } from 'node:child_process'
|
|
16
19
|
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
17
20
|
import { tmpdir } from 'node:os'
|
|
18
21
|
import { dirname, join } from 'node:path'
|
|
19
22
|
import { fileURLToPath } from 'node:url'
|
|
20
23
|
|
|
21
|
-
import {
|
|
24
|
+
import { ensureToolAsync } from './ensure-tool.mjs'
|
|
25
|
+
import { spawnAsync } from '../utils/spawn-async.mjs'
|
|
22
26
|
|
|
23
27
|
/**
|
|
24
28
|
Каталог пакета `@7n/rules`, від якого ресолвимо вшиті директорії правил.
|
|
@@ -44,6 +48,8 @@ const RULES_ROOT = join(PACKAGE_ROOT, 'rules')
|
|
|
44
48
|
* @property {string[]} files список абсолютних шляхів файлів для перевірки (порожній — повертаємо порожньо)
|
|
45
49
|
* @property {string[]} [extraArgs] додаткові аргументи для conftest (наприклад `--combine` для крос-документних правил)
|
|
46
50
|
* @property {object} [templateData] опціональне merged-дерево; серіалізується у JSON `{ "template": <data> }` і передається як `--data <tmpfile>` (cleanup після завершення)
|
|
51
|
+
* @property {AbortSignal} [signal] сигнал скасування — прокидається у `spawnAsync`
|
|
52
|
+
* @property {number} [timeoutMs] ліміт виконання `conftest` у мілісекундах — прокидається у `spawnAsync`
|
|
47
53
|
*/
|
|
48
54
|
|
|
49
55
|
/**
|
|
@@ -65,11 +71,11 @@ export function buildConftestArgs(p) {
|
|
|
65
71
|
* порушень. Якщо `files` порожній — повертає `[]` без спавна. Якщо `conftest`
|
|
66
72
|
* не у PATH і авто-встановлення не вдалось — кидає виняток (hard fail).
|
|
67
73
|
* @param {ConftestBatchOptions} opts параметри запуску
|
|
68
|
-
* @returns {ConftestViolation[]} масив порушень (порожній — все ок)
|
|
74
|
+
* @returns {Promise<ConftestViolation[]>} масив порушень (порожній — все ок)
|
|
69
75
|
*/
|
|
70
|
-
export function runConftestBatch(opts) {
|
|
76
|
+
export async function runConftestBatch(opts) {
|
|
71
77
|
if (opts.files.length === 0) return []
|
|
72
|
-
const conftestBin =
|
|
78
|
+
const conftestBin = await ensureToolAsync('conftest')
|
|
73
79
|
// policyDirRel — формат `<rule>/<concern>` (наприклад `abie/base_deployment_preem`).
|
|
74
80
|
// Flat concern path: rules/<rule>/<concern>/ (без проміжного `policy/`).
|
|
75
81
|
const slash = opts.policyDirRel.indexOf('/')
|
|
@@ -94,11 +100,10 @@ export function runConftestBatch(opts) {
|
|
|
94
100
|
extraArgs: opts.extraArgs ?? [],
|
|
95
101
|
tmpDataFile
|
|
96
102
|
})
|
|
97
|
-
const result =
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
throw new Error(`conftest exit ${result.status}: ${(result.stderr || result.stdout || '').slice(0, 500)}`)
|
|
103
|
+
const result = await spawnAsync(conftestBin, args, { signal: opts.signal, timeoutMs: opts.timeoutMs })
|
|
104
|
+
// conftest exit 1 = є failures (це валідно для нас); >1 (або null — вбито сигналом/таймаутом) = справжня помилка.
|
|
105
|
+
if (result.exitCode !== 0 && result.exitCode !== 1) {
|
|
106
|
+
throw new Error(`conftest exit ${result.exitCode}: ${(result.stderr || result.stdout || '').slice(0, 500)}`)
|
|
102
107
|
}
|
|
103
108
|
/**
|
|
104
109
|
@type {Array<{ filename: string, namespace: string, failures?: Array<{ msg: string }> }>}
|
|
@@ -16,6 +16,7 @@ resource: npm/scripts/utils/
|
|
|
16
16
|
| [resolve-cargo-manifest.mjs](resolve-cargo-manifest.md) | JS Module |
|
|
17
17
|
| [resolve-cmd.mjs](resolve-cmd.md) | JS Module |
|
|
18
18
|
| [resolve-js-root.mjs](resolve-js-root.md) | JS Module |
|
|
19
|
+
| [spawn-async.mjs](spawn-async.md) | JS Module |
|
|
19
20
|
| [test-helpers.mjs](test-helpers.md) | JS Module |
|
|
20
21
|
| [walk-cache.mjs](walk-cache.md) | JS Module |
|
|
21
22
|
| [walkDir.mjs](walkDir.md) | JS Module |
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: JS Module
|
|
3
|
+
title: spawn-async.mjs
|
|
4
|
+
resource: npm/scripts/utils/spawn-async.mjs
|
|
5
|
+
docgen:
|
|
6
|
+
crc: cc5d92e0
|
|
7
|
+
model: openai-codex/gpt-5.4-mini
|
|
8
|
+
tier: cloud-min
|
|
9
|
+
score: 100
|
|
10
|
+
issues: judge:inaccurate:0.98
|
|
11
|
+
judgeModel: openai-codex/gpt-5.4-mini
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Огляд
|
|
15
|
+
|
|
16
|
+
Забезпечує асинхронну заміну `spawnSync` для важких зовнішніх CLI на кшталт `conftest` і `oxlint`, щоб не блокувати Node event loop і не створювати ілюзію паралельності під час запуску кількох detector’ів. Обгортає `child_process.spawn` через `events.once`, підтримує зовнішній `AbortSignal` і `timeoutMs` з ескалацією `SIGTERM` → `SIGKILL`, а результат повертає нормалізовано без винятку на non-zero exit — рішення про це лишається за caller.
|
|
17
|
+
|
|
18
|
+
## Поведінка
|
|
19
|
+
|
|
20
|
+
1. `spawnAsync` запускає зовнішній CLI асинхронно, щоб не блокувати event loop під час важких системних перевірок.
|
|
21
|
+
2. Якщо запуск уже скасовано до старту, одразу повертає помилку скасування.
|
|
22
|
+
3. Збирає `stdout` і `stderr` у нормалізований текстовий результат для подальшого аналізу викликачем.
|
|
23
|
+
4. Підтримує зовнішнє скасування та часовий ліміт: у цих випадках спершу намагається завершити процес м’яко, а потім примусово, якщо він не зупинився в межах grace-періоду.
|
|
24
|
+
5. Повертає код завершення, сигнал завершення та ознаки скасування або timeout як звичайний результат, а не як виняток.
|
|
25
|
+
6. Кидає помилку лише тоді, коли сам запуск процесу не відбувся або середовище не змогло стартувати команду; non-zero exit лишається відповідальністю викликачa.
|
|
26
|
+
|
|
27
|
+
## Публічний API
|
|
28
|
+
|
|
29
|
+
- spawnAsync — асинхронно запускає зовнішню команду, дочікується завершення, збирає її вихід і повертає результат; не падає через code ≠ 0, а кидає лише коли процес не вдалося створити або якщо сигнал уже був aborted до старту
|
|
30
|
+
|
|
31
|
+
## Гарантії поведінки
|
|
32
|
+
|
|
33
|
+
- Read-only: не виконує операцій запису (ФС/БД).
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async (non-blocking) заміна `spawnSync` для важких зовнішніх CLI-викликів (conftest, oxlint тощо).
|
|
3
|
+
*
|
|
4
|
+
* `spawnSync` блокує весь Node event loop цілком — паралельний виклик кількох
|
|
5
|
+
* concern-детекторів навколо `spawnSync` не дає реальної паралельності, лише ілюзію.
|
|
6
|
+
* `spawnAsync` обгортає `child_process.spawn` через `events.once` (без `new Promise`,
|
|
7
|
+
* `promise/avoid-new` заборонений у цьому пакеті), підтримує зовнішній `AbortSignal`
|
|
8
|
+
* і `timeoutMs` (обидва ведуть до `SIGTERM` → ескалація `SIGKILL`, якщо процес не
|
|
9
|
+
* завершився за grace-період), і повертає нормалізований результат без винятку на
|
|
10
|
+
* non-zero exit — це, як і раніше, вирішує caller.
|
|
11
|
+
*/
|
|
12
|
+
import { spawn } from 'node:child_process'
|
|
13
|
+
import { once } from 'node:events'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {object} SpawnAsyncResult
|
|
17
|
+
* @property {string} stdout зібраний stdout (utf8)
|
|
18
|
+
* @property {string} stderr зібраний stderr (utf8)
|
|
19
|
+
* @property {number|null} exitCode код завершення (`null` — процес вбито сигналом)
|
|
20
|
+
* @property {string|null} signal сигнал, яким вбито процес (`null` — завершився сам)
|
|
21
|
+
* @property {boolean} timedOut true, якщо процес вбито через `timeoutMs`
|
|
22
|
+
* @property {boolean} aborted true, якщо процес вбито через зовнішній `AbortSignal`
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** `AbortError` (DOM `AbortController` семантика) для вже-скасованого `signal` до старту спавна. */
|
|
26
|
+
class AbortError extends Error {
|
|
27
|
+
/** @param {string} [message] текст помилки */
|
|
28
|
+
constructor(message = 'The operation was aborted') {
|
|
29
|
+
super(message)
|
|
30
|
+
this.name = 'AbortError'
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Запускає зовнішній процес асинхронно (не блокує event loop) і збирає його результат.
|
|
36
|
+
* Ніколи не кидає на non-zero exit — кидає лише на `spawn`-помилку (ENOENT тощо) або
|
|
37
|
+
* якщо `opts.signal` вже `aborted` до виклику.
|
|
38
|
+
* @param {string} cmd бінарник (шлях або ім'я в PATH)
|
|
39
|
+
* @param {string[]} args аргументи запуску
|
|
40
|
+
* @param {object} [opts] опції
|
|
41
|
+
* @param {AbortSignal} [opts.signal] зовнішній сигнал скасування
|
|
42
|
+
* @param {number} [opts.timeoutMs] ліміт у мілісекундах (без ліміту — не задано / ≤0)
|
|
43
|
+
* @param {number} [opts.killGraceMs] пауза між `SIGTERM` і ескалацією до `SIGKILL` (дефолт 5000)
|
|
44
|
+
* @param {string} [opts.cwd] робочий каталог дочірнього процесу
|
|
45
|
+
* @param {Record<string, string>} [opts.env] оточення дочірнього процесу
|
|
46
|
+
* @returns {Promise<SpawnAsyncResult>} нормалізований результат виконання
|
|
47
|
+
*/
|
|
48
|
+
export async function spawnAsync(cmd, args, opts = {}) {
|
|
49
|
+
const { signal, timeoutMs, killGraceMs = 5000, ...spawnOpts } = opts
|
|
50
|
+
if (signal?.aborted) throw new AbortError()
|
|
51
|
+
|
|
52
|
+
const child = spawn(cmd, args, spawnOpts)
|
|
53
|
+
child.stdout?.setEncoding('utf8')
|
|
54
|
+
child.stderr?.setEncoding('utf8')
|
|
55
|
+
|
|
56
|
+
let stdout = ''
|
|
57
|
+
let stderr = ''
|
|
58
|
+
child.stdout?.on('data', chunk => {
|
|
59
|
+
stdout += chunk
|
|
60
|
+
})
|
|
61
|
+
child.stderr?.on('data', chunk => {
|
|
62
|
+
stderr += chunk
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
let timedOut = false
|
|
66
|
+
let aborted = false
|
|
67
|
+
let settled = false
|
|
68
|
+
let killTimer = null
|
|
69
|
+
let timeoutTimer = null
|
|
70
|
+
|
|
71
|
+
/** SIGTERM негайно, ескалація до SIGKILL якщо процес не завершився за killGraceMs. */
|
|
72
|
+
const killWithEscalation = () => {
|
|
73
|
+
child.kill('SIGTERM')
|
|
74
|
+
killTimer = setTimeout(() => {
|
|
75
|
+
if (!settled) child.kill('SIGKILL')
|
|
76
|
+
}, killGraceMs)
|
|
77
|
+
killTimer.unref?.()
|
|
78
|
+
}
|
|
79
|
+
const onAbort = () => {
|
|
80
|
+
aborted = true
|
|
81
|
+
killWithEscalation()
|
|
82
|
+
}
|
|
83
|
+
if (signal) signal.addEventListener('abort', onAbort)
|
|
84
|
+
if (timeoutMs && timeoutMs > 0) {
|
|
85
|
+
timeoutTimer = setTimeout(() => {
|
|
86
|
+
timedOut = true
|
|
87
|
+
killWithEscalation()
|
|
88
|
+
}, timeoutMs)
|
|
89
|
+
timeoutTimer.unref?.()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** @returns {Promise<{code: number|null, killSignal: string|null}>} результат події `close` */
|
|
93
|
+
const waitForClose = async () => {
|
|
94
|
+
const [code, killSignal] = await once(child, 'close')
|
|
95
|
+
return { code, killSignal }
|
|
96
|
+
}
|
|
97
|
+
/** @returns {Promise<never>} ніколи не резолвиться — кидає подію `error` */
|
|
98
|
+
const waitForError = async () => {
|
|
99
|
+
const [error] = await once(child, 'error')
|
|
100
|
+
throw error
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const { code, killSignal } = await Promise.race([waitForClose(), waitForError()])
|
|
105
|
+
return { stdout, stderr, exitCode: code, signal: killSignal, timedOut, aborted }
|
|
106
|
+
} finally {
|
|
107
|
+
settled = true
|
|
108
|
+
if (killTimer) clearTimeout(killTimer)
|
|
109
|
+
if (timeoutTimer) clearTimeout(timeoutTimer)
|
|
110
|
+
if (signal) signal.removeEventListener('abort', onAbort)
|
|
111
|
+
}
|
|
112
|
+
}
|