inup 1.5.6 → 1.6.2

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.
Files changed (47) hide show
  1. package/README.md +30 -1
  2. package/dist/cli.js +42 -11
  3. package/dist/config/constants.js +6 -7
  4. package/dist/config/project-config.js +5 -0
  5. package/dist/core/package-detector.js +49 -1
  6. package/dist/core/upgrade-runner.js +9 -0
  7. package/dist/core/upgrader.js +9 -3
  8. package/dist/features/changelog/services/package-metadata-service.js +2 -13
  9. package/dist/features/changelog/services/release-notes-service.js +0 -27
  10. package/dist/features/debug/index.js +1 -0
  11. package/dist/features/debug/renderer/performance-modal.js +17 -1
  12. package/dist/features/debug/services/perf-logger.js +129 -0
  13. package/dist/features/debug/services/performance-tracker.js +15 -3
  14. package/dist/features/headless/headless-runner.js +69 -0
  15. package/dist/features/headless/index.js +27 -0
  16. package/dist/features/headless/report.js +64 -0
  17. package/dist/features/headless/types.js +6 -0
  18. package/dist/features/headless/vulnerability-audit.js +106 -0
  19. package/dist/interactive-ui.js +4 -2
  20. package/dist/services/http/adaptive-controller.js +153 -0
  21. package/dist/services/http/etag-store.js +91 -0
  22. package/dist/services/http/resizable-semaphore.js +71 -0
  23. package/dist/services/http/retry.js +23 -1
  24. package/dist/services/index.js +0 -1
  25. package/dist/services/npm-registry.js +198 -97
  26. package/dist/services/package-manager-detector.js +6 -3
  27. package/dist/ui/modal/package-info-sections/sections.js +24 -0
  28. package/dist/ui/presenters/health.js +24 -0
  29. package/dist/ui/renderer/package-list/rows.js +8 -3
  30. package/dist/ui/session/selection-state-builder.js +7 -2
  31. package/dist/ui/themes-colors.js +12 -3
  32. package/dist/ui/utils/cursor.js +9 -3
  33. package/dist/utils/color.js +38 -0
  34. package/dist/utils/debug-logger.js +8 -1
  35. package/dist/utils/engines.js +63 -0
  36. package/dist/utils/filesystem/io.js +16 -0
  37. package/dist/utils/filesystem/scan.js +82 -7
  38. package/dist/utils/index.js +4 -0
  39. package/dist/utils/local-env.js +81 -0
  40. package/dist/utils/manifest.js +35 -0
  41. package/dist/utils/version.js +9 -2
  42. package/package.json +8 -8
  43. package/dist/services/cache-manager.js +0 -95
  44. package/dist/services/jsdelivr/client.js +0 -191
  45. package/dist/services/jsdelivr/manifest.js +0 -136
  46. package/dist/services/jsdelivr-registry.js +0 -9
  47. package/dist/services/persistent-cache.js +0 -237
@@ -6,6 +6,8 @@ class PerformanceTracker {
6
6
  phases = {};
7
7
  counts = {};
8
8
  batches = [];
9
+ controlTicks = [];
10
+ packageTimings = [];
9
11
  failedPackages = [];
10
12
  packageManager = null;
11
13
  start() {
@@ -13,6 +15,8 @@ class PerformanceTracker {
13
15
  this.phases = {};
14
16
  this.counts = {};
15
17
  this.batches = [];
18
+ this.controlTicks = [];
19
+ this.packageTimings = [];
16
20
  this.failedPackages = [];
17
21
  this.packageManager = null;
18
22
  }
@@ -30,6 +34,12 @@ class PerformanceTracker {
30
34
  recordBatch(batch) {
31
35
  this.batches.push(batch);
32
36
  }
37
+ recordControlTick(tick) {
38
+ this.controlTicks.push(tick);
39
+ }
40
+ recordPackageTiming(timing) {
41
+ this.packageTimings.push(timing);
42
+ }
33
43
  recordFailedPackage(name) {
34
44
  if (!this.failedPackages.includes(name)) {
35
45
  this.failedPackages.push(name);
@@ -39,15 +49,15 @@ class PerformanceTracker {
39
49
  this.packageManager = name;
40
50
  }
41
51
  snapshot() {
42
- const totalMs = this.startedAt === null
43
- ? null
44
- : (this.phases.allLoaded ?? Date.now() - this.startedAt);
52
+ const totalMs = this.startedAt === null ? null : (this.phases.allLoaded ?? Date.now() - this.startedAt);
45
53
  return {
46
54
  startedAt: this.startedAt,
47
55
  phases: { ...this.phases },
48
56
  totalMs,
49
57
  counts: { ...this.counts },
50
58
  batches: [...this.batches],
59
+ controlTicks: [...this.controlTicks],
60
+ packageTimings: [...this.packageTimings],
51
61
  failedPackages: [...this.failedPackages],
52
62
  packageManager: this.packageManager,
53
63
  };
@@ -57,6 +67,8 @@ class PerformanceTracker {
57
67
  this.phases = {};
58
68
  this.counts = {};
59
69
  this.batches = [];
70
+ this.controlTicks = [];
71
+ this.packageTimings = [];
60
72
  this.failedPackages = [];
61
73
  this.packageManager = null;
62
74
  }
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.HeadlessRunner = void 0;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const package_detector_1 = require("../../core/package-detector");
9
+ const vulnerability_audit_1 = require("./vulnerability-audit");
10
+ const report_1 = require("./report");
11
+ const debug_1 = require("../../features/debug");
12
+ /**
13
+ * Non-interactive entry point. Resolves the outdated list without rendering the TUI, then either
14
+ * emits a JSON report (--json) or a plain line-based report. With --check, sets a non-zero exit
15
+ * code when updates exist so CI can gate on it. Read-only: never writes package.json or installs.
16
+ *
17
+ * This is the headless counterpart to the interactive `UpgradeRunner`; it shares only the
18
+ * `PackageDetector` (the scan/resolve layer), not the UI/upgrade machinery.
19
+ */
20
+ class HeadlessRunner {
21
+ detector;
22
+ constructor(options) {
23
+ this.detector = new package_detector_1.PackageDetector(options);
24
+ }
25
+ async run(options) {
26
+ try {
27
+ if (!this.detector.hasPackageJson()) {
28
+ throw new Error('No package.json found in current directory');
29
+ }
30
+ // Start perf tracking so headless runs produce clean timing data too
31
+ // (the interactive runner starts it itself; headless previously did not).
32
+ const perfEnabled = (0, debug_1.isPerfLoggingEnabled)();
33
+ const performanceTracker = (0, debug_1.getPerformanceTracker)();
34
+ if (perfEnabled)
35
+ performanceTracker.start();
36
+ const packages = await this.detector.getOutdatedPackages();
37
+ const outdated = this.detector.getOutdatedPackagesOnly(packages);
38
+ if (perfEnabled) {
39
+ performanceTracker.mark('allLoaded');
40
+ (0, debug_1.writePerfLog)({
41
+ ...this.detector.getPerfConfig(),
42
+ packageManager: null,
43
+ mode: 'headless',
44
+ env: (0, debug_1.perfEnv)(),
45
+ }, performanceTracker.snapshot());
46
+ }
47
+ // Audit the current versions (one bulk request, best-effort) and cross-reference each
48
+ // advisory against the upgrade targets, so the report says whether upgrading *fixes* it.
49
+ const vulnerabilities = await (0, vulnerability_audit_1.auditVulnerabilities)(outdated);
50
+ if (options.json) {
51
+ // stdout is reserved for the JSON document only.
52
+ console.log(JSON.stringify((0, report_1.buildHeadlessReport)(packages, outdated, vulnerabilities), null, 2));
53
+ }
54
+ else {
55
+ console.log((0, report_1.renderPlainReport)(outdated, vulnerabilities));
56
+ }
57
+ // Exit 1 only means "updates exist" (like `prettier --check`); 2 is reserved for errors.
58
+ if (options.check && outdated.length > 0) {
59
+ process.exitCode = 1;
60
+ }
61
+ }
62
+ catch (error) {
63
+ console.error(chalk_1.default.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
64
+ process.exit(2);
65
+ }
66
+ }
67
+ }
68
+ exports.HeadlessRunner = HeadlessRunner;
69
+ //# sourceMappingURL=headless-runner.js.map
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.renderPlainReport = exports.buildHeadlessReport = exports.upgradeClears = exports.auditVulnerabilities = exports.HeadlessRunner = void 0;
18
+ __exportStar(require("./types"), exports);
19
+ var headless_runner_1 = require("./headless-runner");
20
+ Object.defineProperty(exports, "HeadlessRunner", { enumerable: true, get: function () { return headless_runner_1.HeadlessRunner; } });
21
+ var vulnerability_audit_1 = require("./vulnerability-audit");
22
+ Object.defineProperty(exports, "auditVulnerabilities", { enumerable: true, get: function () { return vulnerability_audit_1.auditVulnerabilities; } });
23
+ Object.defineProperty(exports, "upgradeClears", { enumerable: true, get: function () { return vulnerability_audit_1.upgradeClears; } });
24
+ var report_1 = require("./report");
25
+ Object.defineProperty(exports, "buildHeadlessReport", { enumerable: true, get: function () { return report_1.buildHeadlessReport; } });
26
+ Object.defineProperty(exports, "renderPlainReport", { enumerable: true, get: function () { return report_1.renderPlainReport; } });
27
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildHeadlessReport = buildHeadlessReport;
4
+ exports.renderPlainReport = renderPlainReport;
5
+ const types_1 = require("./types");
6
+ /** Build the machine-readable `--json` payload from the scanned + outdated package sets. */
7
+ function buildHeadlessReport(all, outdated, vulnerabilities) {
8
+ return {
9
+ schemaVersion: types_1.HEADLESS_SCHEMA_VERSION,
10
+ summary: {
11
+ total: all.length,
12
+ outdated: outdated.length,
13
+ major: outdated.filter((pkg) => pkg.hasMajorUpdate).length,
14
+ vulnerable: vulnerabilities.size,
15
+ },
16
+ outdated: outdated.map((pkg) => {
17
+ const entry = {
18
+ name: pkg.name,
19
+ current: pkg.currentVersion,
20
+ range: pkg.rangeVersion,
21
+ latest: pkg.latestVersion,
22
+ type: pkg.type,
23
+ packageJsonPath: pkg.packageJsonPath,
24
+ hasMajorUpdate: pkg.hasMajorUpdate,
25
+ };
26
+ if (pkg.deprecated)
27
+ entry.deprecated = pkg.deprecated;
28
+ if (pkg.enginesNode)
29
+ entry.enginesNode = pkg.enginesNode;
30
+ const vulnerability = vulnerabilities.get(pkg);
31
+ if (vulnerability)
32
+ entry.vulnerability = vulnerability;
33
+ return entry;
34
+ }),
35
+ };
36
+ }
37
+ /** Render the plain, line-based report (one line per package + a recap) as a single string. */
38
+ function renderPlainReport(outdated, vulnerabilities) {
39
+ if (outdated.length === 0) {
40
+ return 'All dependencies are up to date — no upgrades needed.';
41
+ }
42
+ const lines = outdated.map((pkg) => {
43
+ const major = pkg.hasMajorUpdate ? ' (major)' : '';
44
+ const deprecated = pkg.deprecated ? ' [deprecated]' : '';
45
+ return `${pkg.name} ${pkg.currentVersion} → ${pkg.latestVersion} [${pkg.type}]${major}${vulnMarker(vulnerabilities.get(pkg))}${deprecated}`;
46
+ });
47
+ const fileCount = new Set(outdated.map((pkg) => pkg.packageJsonPath)).size;
48
+ const vulnNote = vulnerabilities.size > 0 ? ` — ${vulnerabilities.size} with known vulnerabilities` : '';
49
+ lines.push('', `${outdated.length} package(s) outdated across ${fileCount} file(s)${vulnNote}.`);
50
+ return lines.join('\n');
51
+ }
52
+ /** A compact `[vuln: N sev → verdict]` tag for the plain report; '' when there are none. */
53
+ function vulnMarker(vulnerability) {
54
+ if (!vulnerability)
55
+ return '';
56
+ // Prefer the cheaper fix: if the in-range bump already clears it, that's the safer action.
57
+ const verdict = vulnerability.fixedByRange
58
+ ? 'fixed by range upgrade'
59
+ : vulnerability.fixedByLatest
60
+ ? 'fixed by latest only'
61
+ : 'not fixed by upgrade';
62
+ return ` [vuln: ${vulnerability.count} ${vulnerability.highestSeverity} → ${verdict}]`;
63
+ }
64
+ //# sourceMappingURL=report.js.map
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HEADLESS_SCHEMA_VERSION = void 0;
4
+ /** Bump when the `--json` shape changes in a way consumers (scripts, agents) must adapt to. */
5
+ exports.HEADLESS_SCHEMA_VERSION = 1;
6
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,106 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.auditVulnerabilities = auditVulnerabilities;
37
+ exports.upgradeClears = upgradeClears;
38
+ const semver = __importStar(require("semver"));
39
+ const services_1 = require("../../services");
40
+ const utils_1 = require("../../utils");
41
+ /**
42
+ * Audit the outdated packages' currently-installed versions (one bulk request, matching the
43
+ * interactive audit) and, for each advisory, cross-reference its affected range against the
44
+ * upgrade targets — so callers can state whether upgrading actually *fixes* the issue.
45
+ *
46
+ * Best-effort: `fetchVulnerabilities` swallows network errors and returns an empty map, so a
47
+ * failed audit never blocks the report. Returns only the vulnerable packages, keyed by package.
48
+ */
49
+ async function auditVulnerabilities(outdated) {
50
+ const result = new Map();
51
+ if (outdated.length === 0)
52
+ return result;
53
+ // The bulk advisory API is keyed by package name (one version per name), so dedupe by name.
54
+ const versions = new Map();
55
+ for (const pkg of outdated) {
56
+ if (!versions.has(pkg.name))
57
+ versions.set(pkg.name, pkg.currentVersion);
58
+ }
59
+ const advisories = await (0, services_1.fetchVulnerabilities)(versions);
60
+ if (advisories.size === 0)
61
+ return result;
62
+ for (const pkg of outdated) {
63
+ const found = advisories.get(pkg.name);
64
+ if (!found || found.vulnerabilities.length === 0 || !found.highestSeverity)
65
+ continue;
66
+ result.set(pkg, summarizeVulnerability(pkg, found.vulnerabilities, found.highestSeverity));
67
+ }
68
+ return result;
69
+ }
70
+ function summarizeVulnerability(pkg, vulnerabilities, highestSeverity) {
71
+ const advisories = vulnerabilities.map((vuln) => ({
72
+ id: vuln.id,
73
+ title: vuln.title,
74
+ severity: vuln.severity,
75
+ url: vuln.url,
76
+ vulnerableVersions: vuln.vulnerable_versions,
77
+ fixedByRange: upgradeClears(pkg.rangeVersion, vuln.vulnerable_versions),
78
+ fixedByLatest: upgradeClears(pkg.latestVersion, vuln.vulnerable_versions),
79
+ }));
80
+ return {
81
+ count: advisories.length,
82
+ highestSeverity,
83
+ fixedByRange: advisories.every((advisory) => advisory.fixedByRange),
84
+ fixedByLatest: advisories.every((advisory) => advisory.fixedByLatest),
85
+ advisories,
86
+ };
87
+ }
88
+ /**
89
+ * True when upgrading to `target` escapes an advisory's affected range. Conservative: if either
90
+ * the target or the advisory range can't be parsed, we do NOT claim a fix — `semver.satisfies`
91
+ * treats an invalid range as "matches nothing", which would otherwise read as a false "fixed".
92
+ */
93
+ function upgradeClears(target, vulnerableVersions) {
94
+ const comparable = (0, utils_1.toComparableVersion)(target);
95
+ if (!comparable)
96
+ return false;
97
+ if (semver.validRange(vulnerableVersions) === null)
98
+ return false;
99
+ try {
100
+ return !semver.satisfies(comparable, vulnerableVersions, { includePrerelease: true });
101
+ }
102
+ catch {
103
+ return false;
104
+ }
105
+ }
106
+ //# sourceMappingURL=vulnerability-audit.js.map
@@ -20,6 +20,7 @@ class InteractiveUI {
20
20
  renderer;
21
21
  packageManager;
22
22
  options;
23
+ saveExact;
23
24
  vulnerabilityAuditController = new controllers_1.VulnerabilityAuditController();
24
25
  packageInfoModalController = new controllers_1.PackageInfoModalController();
25
26
  refreshView;
@@ -27,6 +28,7 @@ class InteractiveUI {
27
28
  this.renderer = new ui_1.UIRenderer();
28
29
  this.packageManager = packageManager;
29
30
  this.options = normalizeVulnerabilityDisplayOptions(options);
31
+ this.saveExact = options?.saveExact ?? false;
30
32
  }
31
33
  async displayPackagesTable(packages) {
32
34
  console.log(this.renderer.renderPackagesTable(packages));
@@ -37,7 +39,7 @@ class InteractiveUI {
37
39
  return [];
38
40
  }
39
41
  const selectedStates = await (0, session_1.runInteractiveSession)(selectionStates, this.packageManager, this.renderer, this.packageInfoModalController, this.vulnerabilityAuditController, this.options, (refresh) => { this.refreshView = refresh; });
40
- return (0, session_1.createUpgradeChoices)(selectedStates);
42
+ return (0, session_1.createUpgradeChoices)(selectedStates, this.saveExact);
41
43
  }
42
44
  createSelectionStates(packages, previousSelections, includeUpToDate = true) {
43
45
  return (0, session_1.createSelectionStates)(packages, (name, version, type) => this.vulnerabilityAuditController.getCachedSummary(name, version, type), previousSelections, includeUpToDate);
@@ -63,7 +65,7 @@ class InteractiveUI {
63
65
  async selectPackagesToUpgradeProgressive(selectionStates, progress, attachRefresh) {
64
66
  this.enqueueSecurityAudit(selectionStates);
65
67
  const selectedStates = await (0, session_1.runInteractiveSession)(selectionStates, this.packageManager, this.renderer, this.packageInfoModalController, this.vulnerabilityAuditController, this.options, (refresh) => { this.refreshView = refresh; }, progress, attachRefresh);
66
- return (0, session_1.createUpgradeChoices)(selectedStates);
68
+ return (0, session_1.createUpgradeChoices)(selectedStates, this.saveExact);
67
69
  }
68
70
  enqueueSecurityAudit(selectionStates) {
69
71
  this.vulnerabilityAuditController.enqueueStates(selectionStates, () => this.refreshView?.());
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ /**
3
+ * AIMD (additive-increase / multiplicative-decrease) concurrency controller.
4
+ *
5
+ * Mirrors the congestion-control family TCP uses. It performs no I/O and does not
6
+ * own the semaphore — it is a pure decision function: feed it the outcome of each
7
+ * request, and on each control tick it returns the next concurrency limit, which
8
+ * the caller applies to the resizable semaphore.
9
+ *
10
+ * Back-off signals (only real errors move the limit down):
11
+ * - congestion (HTTP 429/503): the registry explicitly telling us to slow down →
12
+ * immediate hard multiplicative-decrease (×hardDecreaseFactor), the moment the
13
+ * signal arrives, not deferred to the next tick.
14
+ * - retryable/transient errors in a tick window → soft multiplicative-decrease
15
+ * (×softDecreaseFactor) and no increase.
16
+ *
17
+ * Otherwise (healthy link) the limit ramps additively (+increaseStep) toward the
18
+ * ceiling and holds there.
19
+ *
20
+ * NOTE on latency: we sample successful single-attempt latency into an EWMA, but
21
+ * it is used for *instrumentation only* — never to drive back-off. The npm
22
+ * registry is a CDN whose latency varies widely on a perfectly healthy link;
23
+ * reacting to that variance made the controller oscillate and lose to a fixed
24
+ * limit. Real congestion shows up as 429, which we handle directly.
25
+ */
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.AdaptiveController = exports.DEFAULT_TUNING = void 0;
28
+ exports.DEFAULT_TUNING = {
29
+ floor: 6,
30
+ ceil: 24,
31
+ increaseStep: 4,
32
+ softDecreaseFactor: 0.7,
33
+ hardDecreaseFactor: 0.5,
34
+ ewmaAlpha: 0.3,
35
+ ticksEveryCompletions: 6,
36
+ };
37
+ const clamp = (value, lo, hi) => Math.max(lo, Math.min(hi, value));
38
+ class AdaptiveController {
39
+ onTick;
40
+ tuning;
41
+ limit;
42
+ // EWMA of successful single-attempt latency. Kept for instrumentation only
43
+ // (the perf modal / logs); it does not drive concurrency decisions.
44
+ ewmaMs = 0;
45
+ hasEwma = false;
46
+ completionsSinceTick = 0;
47
+ retriesSinceTick = 0;
48
+ /**
49
+ * @param packageCount number of packages this run will fetch; drives the smart
50
+ * initial limit so a run starts near its work size and
51
+ * ramps to the ceiling, rather than crawling up from the
52
+ * floor and losing to a fixed baseline.
53
+ * @param onTick optional sink for control decisions (perf modal / logs).
54
+ * @param tuning override the defaults (constants live in one place).
55
+ */
56
+ constructor(packageCount, onTick, tuning = {}) {
57
+ this.onTick = onTick;
58
+ this.tuning = { ...exports.DEFAULT_TUNING, ...tuning };
59
+ // Smart start: aim at the work size, clamped to [floor, ceil].
60
+ this.limit = clamp(Math.min(this.tuning.ceil, packageCount), this.tuning.floor, this.tuning.ceil);
61
+ }
62
+ /** Whether the controller should even run; tiny runs are better off fixed. */
63
+ static shouldControl(packageCount, tuning = {}) {
64
+ const ceil = tuning.ceil ?? exports.DEFAULT_TUNING.ceil;
65
+ // Nothing to ramp toward and no steady state to learn below the ceiling.
66
+ return packageCount > ceil;
67
+ }
68
+ getLimit() {
69
+ return this.limit;
70
+ }
71
+ /**
72
+ * Record a completed request. Returns a new limit to apply IMMEDIATELY when a
73
+ * congestion signal demands a hard back-off, otherwise null (the limit may
74
+ * still change at the next control tick — see `maybeTick`).
75
+ */
76
+ record(kind, latencyMs) {
77
+ this.completionsSinceTick++;
78
+ if (kind === 'success' && latencyMs !== undefined) {
79
+ this.sampleLatency(latencyMs);
80
+ return null;
81
+ }
82
+ if (kind === 'congested') {
83
+ this.retriesSinceTick++;
84
+ return this.applyHardDecrease();
85
+ }
86
+ // retryable / transient
87
+ this.retriesSinceTick++;
88
+ return null;
89
+ }
90
+ /**
91
+ * Call after each completion. If a control tick is due, computes and returns
92
+ * the next limit (and emits a ControlTick); otherwise returns null.
93
+ */
94
+ maybeTick(now = Date.now()) {
95
+ if (this.completionsSinceTick < this.tuning.ticksEveryCompletions) {
96
+ return null;
97
+ }
98
+ return this.tick(now);
99
+ }
100
+ sampleLatency(latencyMs) {
101
+ if (!this.hasEwma) {
102
+ this.ewmaMs = latencyMs;
103
+ this.hasEwma = true;
104
+ }
105
+ else {
106
+ const a = this.tuning.ewmaAlpha;
107
+ this.ewmaMs = a * latencyMs + (1 - a) * this.ewmaMs;
108
+ }
109
+ }
110
+ applyHardDecrease() {
111
+ const next = clamp(Math.round(this.limit * this.tuning.hardDecreaseFactor), this.tuning.floor, this.tuning.ceil);
112
+ this.limit = next;
113
+ this.emit('hard-down');
114
+ // A hard decrease resets the tick window so we don't immediately bump back up.
115
+ this.resetWindow();
116
+ return next;
117
+ }
118
+ tick(now) {
119
+ // Back off only on real errors seen since the last tick; otherwise ramp to
120
+ // the ceiling and hold (latency is not a signal — see the class docblock).
121
+ const retries = this.retriesSinceTick;
122
+ let reason;
123
+ if (retries > 0) {
124
+ this.limit = clamp(Math.round(this.limit * this.tuning.softDecreaseFactor), this.tuning.floor, this.tuning.ceil);
125
+ reason = 'soft-down';
126
+ }
127
+ else if (this.limit < this.tuning.ceil) {
128
+ this.limit = clamp(this.limit + this.tuning.increaseStep, this.tuning.floor, this.tuning.ceil);
129
+ reason = 'up';
130
+ }
131
+ else {
132
+ reason = 'hold';
133
+ }
134
+ this.emit(reason, now);
135
+ this.resetWindow();
136
+ return this.limit;
137
+ }
138
+ emit(reason, now = Date.now()) {
139
+ this.onTick?.({
140
+ atMs: now,
141
+ limit: this.limit,
142
+ ewmaMs: Math.round(this.ewmaMs),
143
+ retries: this.retriesSinceTick,
144
+ reason,
145
+ });
146
+ }
147
+ resetWindow() {
148
+ this.completionsSinceTick = 0;
149
+ this.retriesSinceTick = 0;
150
+ }
151
+ }
152
+ exports.AdaptiveController = AdaptiveController;
153
+ //# sourceMappingURL=adaptive-controller.js.map
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setEtagCacheEnabled = setEtagCacheEnabled;
4
+ exports.readEtag = readEtag;
5
+ exports.writeEtag = writeEtag;
6
+ exports.etagCacheDir = etagCacheDir;
7
+ const node_crypto_1 = require("node:crypto");
8
+ const node_fs_1 = require("node:fs");
9
+ const node_os_1 = require("node:os");
10
+ const node_path_1 = require("node:path");
11
+ /** Bump when the on-disk entry shape changes; old generations are ignored. */
12
+ const SCHEMA = 'v1';
13
+ /** Entries untouched for longer than this are swept on first access. */
14
+ const MAX_AGE_MS = 14 * 24 * 60 * 60 * 1000; // 14 days
15
+ let enabled = true;
16
+ let sweptThisProcess = false;
17
+ /** Allow tests/callers to toggle the store without touching call sites. */
18
+ function setEtagCacheEnabled(value) {
19
+ enabled = value;
20
+ }
21
+ /** Resolve (and lazily create) the cache directory, sweeping stale entries once. */
22
+ function cacheDir() {
23
+ // Version data is not project-specific, so it lives in the OS temp area, shared
24
+ // across every project the user scans.
25
+ const dir = (0, node_path_1.join)((0, node_os_1.tmpdir)(), 'inup', 'etag-cache', SCHEMA);
26
+ if (!(0, node_fs_1.existsSync)(dir)) {
27
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true });
28
+ }
29
+ if (!sweptThisProcess) {
30
+ sweptThisProcess = true;
31
+ sweepStale(dir);
32
+ }
33
+ return dir;
34
+ }
35
+ /** Delete entries not modified within MAX_AGE_MS. Best-effort, runs once. */
36
+ function sweepStale(dir) {
37
+ try {
38
+ const cutoff = Date.now() - MAX_AGE_MS;
39
+ for (const name of (0, node_fs_1.readdirSync)(dir)) {
40
+ const file = (0, node_path_1.join)(dir, name);
41
+ try {
42
+ if ((0, node_fs_1.statSync)(file).mtimeMs < cutoff)
43
+ (0, node_fs_1.unlinkSync)(file);
44
+ }
45
+ catch {
46
+ /* skip files we can't stat/remove */
47
+ }
48
+ }
49
+ }
50
+ catch {
51
+ /* sweeping is optional; never fail a run over it */
52
+ }
53
+ }
54
+ /** Stable, filesystem-safe filename for a cache key (the registry path). */
55
+ function fileFor(key) {
56
+ const hash = (0, node_crypto_1.createHash)('sha1').update(key).digest('hex');
57
+ return (0, node_path_1.join)(cacheDir(), `${hash}.json`);
58
+ }
59
+ /** Read a stored entry, or null if absent/unreadable/disabled. */
60
+ function readEtag(key) {
61
+ if (!enabled)
62
+ return null;
63
+ try {
64
+ const file = fileFor(key);
65
+ if (!(0, node_fs_1.existsSync)(file))
66
+ return null;
67
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(file, 'utf8'));
68
+ if (!parsed || typeof parsed.etag !== 'string' || !parsed.data)
69
+ return null;
70
+ return parsed;
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ }
76
+ /** Persist an entry. Best-effort; failures are ignored. */
77
+ function writeEtag(key, etag, data) {
78
+ if (!enabled || !etag)
79
+ return;
80
+ try {
81
+ (0, node_fs_1.writeFileSync)(fileFor(key), JSON.stringify({ etag, data }));
82
+ }
83
+ catch {
84
+ /* best-effort */
85
+ }
86
+ }
87
+ /** Test/maintenance helper: the resolved cache directory. */
88
+ function etagCacheDir() {
89
+ return cacheDir();
90
+ }
91
+ //# sourceMappingURL=etag-store.js.map