inup 1.6.0 → 1.6.3
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/README.md +81 -4
- package/dist/cli.js +23 -3
- package/dist/config/constants.js +6 -3
- package/dist/core/package-detector.js +20 -0
- package/dist/core/upgrade-runner.js +8 -0
- package/dist/core/upgrader.js +36 -15
- package/dist/features/debug/index.js +1 -0
- package/dist/features/debug/renderer/performance-modal.js +17 -1
- package/dist/features/debug/services/perf-logger.js +129 -0
- package/dist/features/debug/services/performance-tracker.js +15 -3
- package/dist/features/headless/headless-runner.js +89 -5
- package/dist/services/http/adaptive-controller.js +153 -0
- package/dist/services/http/etag-store.js +91 -0
- package/dist/services/http/resizable-semaphore.js +71 -0
- package/dist/services/http/retry.js +23 -1
- package/dist/services/npm-registry.js +195 -86
- package/dist/services/package-manager-detector.js +4 -0
- package/dist/utils/debug-logger.js +8 -1
- package/dist/utils/filesystem/scan.js +13 -2
- package/dist/utils/index.js +1 -0
- package/dist/utils/local-env.js +81 -0
- package/package.json +7 -10
- package/dist/services/cache-manager.js +0 -95
- package/dist/services/persistent-cache.js +0 -237
|
@@ -6,19 +6,26 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.HeadlessRunner = void 0;
|
|
7
7
|
const chalk_1 = __importDefault(require("chalk"));
|
|
8
8
|
const package_detector_1 = require("../../core/package-detector");
|
|
9
|
+
const upgrader_1 = require("../../core/upgrader");
|
|
10
|
+
const package_manager_detector_1 = require("../../services/package-manager-detector");
|
|
11
|
+
const version_1 = require("../../ui/utils/version");
|
|
9
12
|
const vulnerability_audit_1 = require("./vulnerability-audit");
|
|
10
13
|
const report_1 = require("./report");
|
|
14
|
+
const debug_1 = require("../../features/debug");
|
|
11
15
|
/**
|
|
12
16
|
* Non-interactive entry point. Resolves the outdated list without rendering the TUI, then either
|
|
13
17
|
* emits a JSON report (--json) or a plain line-based report. With --check, sets a non-zero exit
|
|
14
|
-
* code when updates exist so CI can gate on it.
|
|
18
|
+
* code when updates exist so CI can gate on it. With --apply, writes the bumps to package.json and
|
|
19
|
+
* runs install — the only non-interactive write path.
|
|
15
20
|
*
|
|
16
|
-
* This is the headless counterpart to the interactive `UpgradeRunner`; it shares
|
|
17
|
-
* `PackageDetector` (
|
|
21
|
+
* This is the headless counterpart to the interactive `UpgradeRunner`; it shares the
|
|
22
|
+
* `PackageDetector` (scan/resolve) and, for --apply, the `PackageUpgrader` (write + install).
|
|
18
23
|
*/
|
|
19
24
|
class HeadlessRunner {
|
|
20
25
|
detector;
|
|
26
|
+
options;
|
|
21
27
|
constructor(options) {
|
|
28
|
+
this.options = options;
|
|
22
29
|
this.detector = new package_detector_1.PackageDetector(options);
|
|
23
30
|
}
|
|
24
31
|
async run(options) {
|
|
@@ -26,16 +33,38 @@ class HeadlessRunner {
|
|
|
26
33
|
if (!this.detector.hasPackageJson()) {
|
|
27
34
|
throw new Error('No package.json found in current directory');
|
|
28
35
|
}
|
|
36
|
+
// Start perf tracking so headless runs produce clean timing data too
|
|
37
|
+
// (the interactive runner starts it itself; headless previously did not).
|
|
38
|
+
const perfEnabled = (0, debug_1.isPerfLoggingEnabled)();
|
|
39
|
+
const performanceTracker = (0, debug_1.getPerformanceTracker)();
|
|
40
|
+
if (perfEnabled)
|
|
41
|
+
performanceTracker.start();
|
|
29
42
|
const packages = await this.detector.getOutdatedPackages();
|
|
30
43
|
const outdated = this.detector.getOutdatedPackagesOnly(packages);
|
|
44
|
+
if (perfEnabled) {
|
|
45
|
+
performanceTracker.mark('allLoaded');
|
|
46
|
+
(0, debug_1.writePerfLog)({
|
|
47
|
+
...this.detector.getPerfConfig(),
|
|
48
|
+
packageManager: null,
|
|
49
|
+
mode: 'headless',
|
|
50
|
+
env: (0, debug_1.perfEnv)(),
|
|
51
|
+
}, performanceTracker.snapshot());
|
|
52
|
+
}
|
|
31
53
|
// Audit the current versions (one bulk request, best-effort) and cross-reference each
|
|
32
54
|
// advisory against the upgrade targets, so the report says whether upgrading *fixes* it.
|
|
33
55
|
const vulnerabilities = await (0, vulnerability_audit_1.auditVulnerabilities)(outdated);
|
|
56
|
+
// Build the report from the *pre-apply* outdated set: it describes what this run addressed.
|
|
57
|
+
const report = (0, report_1.buildHeadlessReport)(packages, outdated, vulnerabilities);
|
|
58
|
+
// --apply writes the bumps + lockfile. The scan above already honored .inuprc
|
|
59
|
+
// (ignore/exclude/scanDirs), so the set we write is exactly the set we report — never more.
|
|
60
|
+
if (options.apply) {
|
|
61
|
+
await this.applyUpgrades(outdated, options.target ?? 'minor', !!options.json);
|
|
62
|
+
}
|
|
34
63
|
if (options.json) {
|
|
35
64
|
// stdout is reserved for the JSON document only.
|
|
36
|
-
console.log(JSON.stringify(
|
|
65
|
+
console.log(JSON.stringify(report, null, 2));
|
|
37
66
|
}
|
|
38
|
-
else {
|
|
67
|
+
else if (!options.apply) {
|
|
39
68
|
console.log((0, report_1.renderPlainReport)(outdated, vulnerabilities));
|
|
40
69
|
}
|
|
41
70
|
// Exit 1 only means "updates exist" (like `prettier --check`); 2 is reserved for errors.
|
|
@@ -48,6 +77,61 @@ class HeadlessRunner {
|
|
|
48
77
|
process.exit(2);
|
|
49
78
|
}
|
|
50
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Apply upgrades to the already-filtered outdated set, by version policy. Reuses the interactive
|
|
82
|
+
* write path (`PackageUpgrader`) verbatim — we only build the choices programmatically instead of
|
|
83
|
+
* from a TUI selection. No path/package filtering happens here: `outdated` is config-filtered.
|
|
84
|
+
*/
|
|
85
|
+
async applyUpgrades(outdated, target, json) {
|
|
86
|
+
const choices = this.buildChoices(outdated, target);
|
|
87
|
+
if (choices.length === 0)
|
|
88
|
+
return;
|
|
89
|
+
const packageManager = this.resolvePackageManager();
|
|
90
|
+
// With --json, run the upgrader quietly: its own progress + the install child's stdout go to
|
|
91
|
+
// stderr, leaving stdout for the JSON document only.
|
|
92
|
+
const upgrader = new upgrader_1.PackageUpgrader(packageManager, { quiet: json });
|
|
93
|
+
await upgrader.upgradePackages(choices, outdated);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Build `PackageUpgradeChoice[]` from the outdated set per the version policy. Mirrors
|
|
97
|
+
* `createUpgradeChoices` in the TUI: preserves the original range prefix (^/~) unless --save-exact.
|
|
98
|
+
*
|
|
99
|
+
* - minor/patch: take the in-range target (`rangeVersion`); skip packages whose only update is a
|
|
100
|
+
* major (no in-range bump). Uses upgradeType 'range'.
|
|
101
|
+
* - latest: take `latestVersion`; uses upgradeType 'latest' (majors included).
|
|
102
|
+
*/
|
|
103
|
+
buildChoices(outdated, target) {
|
|
104
|
+
const saveExact = this.options?.saveExact ?? false;
|
|
105
|
+
const choices = [];
|
|
106
|
+
for (const pkg of outdated) {
|
|
107
|
+
const useLatest = target === 'latest';
|
|
108
|
+
// minor/patch only act on packages with an in-range bump; major-only updates are skipped.
|
|
109
|
+
if (!useLatest && !pkg.hasRangeUpdate)
|
|
110
|
+
continue;
|
|
111
|
+
const targetVersion = useLatest ? pkg.latestVersion : pkg.rangeVersion;
|
|
112
|
+
if (!targetVersion)
|
|
113
|
+
continue;
|
|
114
|
+
const targetVersionWithPrefix = saveExact
|
|
115
|
+
? targetVersion
|
|
116
|
+
: (0, version_1.applyVersionPrefix)(pkg.currentVersion, targetVersion);
|
|
117
|
+
choices.push({
|
|
118
|
+
name: pkg.name,
|
|
119
|
+
packageJsonPath: pkg.packageJsonPath,
|
|
120
|
+
dependencyType: pkg.type,
|
|
121
|
+
upgradeType: useLatest ? 'latest' : 'range',
|
|
122
|
+
targetVersion: targetVersionWithPrefix,
|
|
123
|
+
currentVersionSpecifier: pkg.currentVersion,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return choices;
|
|
127
|
+
}
|
|
128
|
+
/** Resolve the package manager the same way the interactive runner does. */
|
|
129
|
+
resolvePackageManager() {
|
|
130
|
+
const cwd = this.options?.cwd || process.cwd();
|
|
131
|
+
return this.options?.packageManager
|
|
132
|
+
? package_manager_detector_1.PackageManagerDetector.getInfo(this.options.packageManager)
|
|
133
|
+
: package_manager_detector_1.PackageManagerDetector.detect(cwd);
|
|
134
|
+
}
|
|
51
135
|
}
|
|
52
136
|
exports.HeadlessRunner = HeadlessRunner;
|
|
53
137
|
//# sourceMappingURL=headless-runner.js.map
|
|
@@ -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
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ResizableSemaphore = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* A counting semaphore whose limit can change at runtime.
|
|
6
|
+
*
|
|
7
|
+
* - `acquire()` resolves immediately if there is headroom, otherwise queues a
|
|
8
|
+
* waiter (FIFO) until a `release()` opens a slot.
|
|
9
|
+
* - `setLimit()` may grow or shrink the limit at any time. Growing wakes as many
|
|
10
|
+
* queued waiters as the new headroom allows. Shrinking never aborts in-flight
|
|
11
|
+
* holders — it simply stops admitting new ones until natural releases drain
|
|
12
|
+
* `inFlight` back below the new limit.
|
|
13
|
+
* - `release()` is guarded: it only wakes a waiter when `inFlight < limit`. This
|
|
14
|
+
* is what makes a shrink actually take effect (an unconditional wake would
|
|
15
|
+
* re-admit immediately and ignore the smaller limit).
|
|
16
|
+
*
|
|
17
|
+
* Invariant: `inFlight` never exceeds `limit` at the moment of admission. A prior
|
|
18
|
+
* shrink can leave `inFlight > limit` transiently; no new holder is admitted
|
|
19
|
+
* until it drains.
|
|
20
|
+
*/
|
|
21
|
+
class ResizableSemaphore {
|
|
22
|
+
limit;
|
|
23
|
+
inFlight = 0;
|
|
24
|
+
waiters = [];
|
|
25
|
+
constructor(initialLimit) {
|
|
26
|
+
this.limit = Math.max(1, Math.floor(initialLimit));
|
|
27
|
+
}
|
|
28
|
+
getLimit() {
|
|
29
|
+
return this.limit;
|
|
30
|
+
}
|
|
31
|
+
getInFlight() {
|
|
32
|
+
return this.inFlight;
|
|
33
|
+
}
|
|
34
|
+
getWaiterCount() {
|
|
35
|
+
return this.waiters.length;
|
|
36
|
+
}
|
|
37
|
+
setLimit(next) {
|
|
38
|
+
this.limit = Math.max(1, Math.floor(next));
|
|
39
|
+
// Growing may have opened headroom for queued waiters.
|
|
40
|
+
this.drainWaiters();
|
|
41
|
+
}
|
|
42
|
+
async acquire() {
|
|
43
|
+
// `inFlight` is the single authoritative counter and is incremented exactly
|
|
44
|
+
// once per admission, synchronously at the moment admission is decided —
|
|
45
|
+
// either here (fast path) or in `drainWaiters` (when a slot frees up). A
|
|
46
|
+
// woken waiter must NOT increment again, or a concurrent fast-path acquire
|
|
47
|
+
// reading a stale count could over-admit past the limit.
|
|
48
|
+
if (this.inFlight < this.limit) {
|
|
49
|
+
this.inFlight++;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
await new Promise((resolve) => this.waiters.push(resolve));
|
|
53
|
+
}
|
|
54
|
+
release() {
|
|
55
|
+
this.inFlight--;
|
|
56
|
+
this.drainWaiters();
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Wakes queued waiters while there is headroom, incrementing `inFlight` for
|
|
60
|
+
* each as it is admitted so the count stays authoritative and synchronous.
|
|
61
|
+
*/
|
|
62
|
+
drainWaiters() {
|
|
63
|
+
while (this.inFlight < this.limit && this.waiters.length > 0) {
|
|
64
|
+
const next = this.waiters.shift();
|
|
65
|
+
this.inFlight++;
|
|
66
|
+
next();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.ResizableSemaphore = ResizableSemaphore;
|
|
71
|
+
//# sourceMappingURL=resizable-semaphore.js.map
|
|
@@ -1,10 +1,32 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isTransientNetworkError = exports.isRetryableStatus = exports.sleep = void 0;
|
|
3
|
+
exports.isTransientNetworkError = exports.parseRetryAfterMs = exports.isCongestionStatus = exports.isRetryableStatus = exports.sleep = void 0;
|
|
4
4
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
5
5
|
exports.sleep = sleep;
|
|
6
6
|
const isRetryableStatus = (statusCode) => statusCode === 408 || statusCode === 429 || statusCode >= 500;
|
|
7
7
|
exports.isRetryableStatus = isRetryableStatus;
|
|
8
|
+
// Registry explicitly signaling congestion: the highest-quality back-off signal.
|
|
9
|
+
const isCongestionStatus = (statusCode) => statusCode === 429 || statusCode === 503;
|
|
10
|
+
exports.isCongestionStatus = isCongestionStatus;
|
|
11
|
+
// Parse a Retry-After header (delta-seconds or HTTP-date) into milliseconds.
|
|
12
|
+
// Returns null when absent or unparseable.
|
|
13
|
+
const parseRetryAfterMs = (header, now = Date.now()) => {
|
|
14
|
+
if (header === undefined)
|
|
15
|
+
return null;
|
|
16
|
+
const value = (Array.isArray(header) ? header[0] : header)?.toString().trim();
|
|
17
|
+
if (!value)
|
|
18
|
+
return null;
|
|
19
|
+
const asSeconds = Number(value);
|
|
20
|
+
if (Number.isFinite(asSeconds)) {
|
|
21
|
+
return asSeconds >= 0 ? Math.round(asSeconds * 1000) : null;
|
|
22
|
+
}
|
|
23
|
+
const asDate = Date.parse(value);
|
|
24
|
+
if (Number.isFinite(asDate)) {
|
|
25
|
+
return Math.max(0, asDate - now);
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
};
|
|
29
|
+
exports.parseRetryAfterMs = parseRetryAfterMs;
|
|
8
30
|
const isTransientNetworkError = (error) => {
|
|
9
31
|
if (!(error instanceof Error)) {
|
|
10
32
|
return false;
|