The parser will need to move between proxies, so this adds the module it will sit on: pluggable sources, a pool that hands proxies out and learns from the outcome, three rotation strategies, and a page to drive it. Sources are IProxySource implementations. The public proxifly/free-proxy-list feed is fetched as the combined all/data.json through jsDelivr and filtered locally — one conditional request beats four per-protocol ones that can disagree mid-publish — and cached for the five minutes upstream takes to regenerate. A feed that is down keeps serving its last payload rather than emptying the pool. The user's own list lives in proxies.custom.json beside the settings, takes a pasted blob, and names the lines it could not parse instead of quietly dropping them. Both knobs the pool exposes are settings, as asked: rotation is Sticky (default, the only one that keeps site sessions coherent), RoundRobin or WeightedRandom; liveness is either a parallel sweep of the whole pool or a probe at hand-out time. Free lists are a few percent alive, so skipping verification entirely means mostly waiting on timeouts. Two invariants worth keeping, both of which cost a bug to find: Availability is decided by the quarantine, never by Health. Excluding everything that has ever failed made the quarantine window dead code and discarded proxies permanently on their first hiccup, which is exactly wrong for addresses that flap constantly. Health only orders the candidates now. A probe verdict does not touch the success/failure counters. Those are about real requests, and letting a sweep over a few thousand proxies rewrite them would drown the evidence weighted selection reads. SOCKS needs no extra package — .NET resolves socks4/socks4a/socks5 in WebProxy — but a proxifly record with "protocol": "https" is still an HTTP proxy reached over http:// with CONNECT, not an https:// scheme. 115 new tests. Also fixes a pre-existing flake: a command gated on another command's IsExecuting cannot be driven straight after its Execute() completes, because IsExecuting is published on the output scheduler. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
11 KiB
CLAUDE.md
Конвенции этого репозитория и грабли, на которые здесь уже наступили. Читать до правок.
Команды
Полный локальный гейт (формат → сборка → тесты) — то, что нужно прогнать перед коммитом:
./build.ps1
Отдельные шаги, если нужен только один из них:
dotnet build AvParser.slnx -c Release
dotnet test AvParser.slnx -c Release
dotnet csharpier check .
./run.ps1
На Linux/macOS — ./build.sh и ./run.sh с теми же шагами.
Слои
Core ← Infrastructure ← UI ← Desktop, строго в одну сторону.
AvParser.Coreне ссылается на Avalonia. Это единственное ограничение, которое здесь по-настоящему несущее: домен должен запускаться из CLI, worker-сервиса или бенчмарка. Как только Avalonia станет доступна из домена, кто-нибудь потянется кDispatcher.UIThreadилиIStorageProviderвнутри парсера.AvParser.UI— библиотека, а не exe. Headless-тесты собирают настоящие View, не подтягиваяProgram.cs, Serilog и контейнер.AvParser.Desktop— тонкий composition root. Логика туда не переезжает.
Добавить парсер
- Реализовать
ITextParserвCore/Parsing/. - Одна строка в
CoreServiceCollectionExtensions.AddAvParserCore().
Всё. IParserCatalog, страница Parse и выпадающий список подхватят его сами.
Добавить страницу
- Наследник
PageViewModelвUI/ViewModels/XxxViewModel.cs(Title,IconKey). UI/Views/XxxView.axaml— имя обязано соответствовать конвенцииViewLocator:...ViewModels.XxxViewModel→...Views.XxxView.- Регистрация в
AddAvParserUI(): конкретным типом и какPageViewModel— порядок этих регистраций и есть порядок пунктов в рельсе навигации.
ReactiveUI 24 (дистрибутив Primitives)
Это не классический ReactiveUI. System.Reactive не используется:
| Классика | Здесь |
|---|---|
Unit |
RxVoid |
IScheduler |
ISequencer (ReactiveUI.Primitives.Concurrency) |
Subject<T> / BehaviorSubject<T> |
Signal<T> / BehaviorSignal<T> |
RxApp.MainThreadScheduler |
RxSchedulers.MainThreadScheduler |
TestScheduler |
VirtualClock, ImmediateSequencer.Instance |
Привычные имена операторов (Select, Where, Throttle, DistinctUntilChanged,
CombineLatest) работают — Primitives отдаёт оба набора. using ReactiveUI.Primitives;
нужен ради Subscribe(Action<T>).
ReactiveUI 24 не инициализируется сама. Первый WhenAnyValue бросит
InvalidOperationException, пока не отработал builder. В приложении это делает
AppBuilder.UseReactiveUI(...); в проекте VM-тестов — module initializer
ReactiveUiBootstrap. Новый тестовый проект без Avalonia обязан сделать то же самое.
Конвенции ViewModel
- Каждая VM принимает
ISequencer? mainThread = nullи использует его вoutputScheduler:иToProperty(..., scheduler). Именно это делает тесты синхронными: они передаютImmediateSequencer.Instance. Без этого пришлось бы гонять диспетчер. - У VM с необязательным
ISequencerрегистрация в DI — явная фабрика, а не по типу: иначе выбор конструктора контейнером зависит от порядка регистраций. [Reactive]изReactiveUI.SourceGeneratorsна partial-свойствах; класс —partial.
Добавить источник прокси
- Реализовать
IProxySource(илиIMutableProxySource, если список редактируемый). - Зарегистрировать как
IProxySourceвAddAvParserProxies(). Порядок регистрации = порядок слияния; свой список идёт последним, чтобы пользовательский адрес перебивал фидовый.
ProxyPool при обновлении переиспользует существующие ProxyEntry по Endpoint.Key — иначе
перезагрузка списка стирала бы всю накопленную статистику, а публичные фиды переиздаются каждые
несколько минут.
Инварианты прокси-пула
- Доступность определяется карантином, а не
Health.Health— это «что видели в последний раз». Если исключать всё, что когда-либо падало, окно карантина становится бессмысленным, а прокси теряется навсегда после первой же осечки. Это уже был баг, его ловитA_failing_proxy_is_quarantined_and_comes_back_later. - Проба не трогает
SuccessCount/FailureCount. Эти счётчики про реальные запросы; свип по паре тысяч прокси перезаписал бы всё, на чём держится взвешенный выбор. Провалившаяся проба выставляет карантин черезRecordProbe(..., quarantineOnFailure:). - Лиза без вердикта нейтральна. Отменённая операция — не вина прокси; считать это отказом значит карантинить здоровые прокси на каждый Cancel.
SelectиNext— зарезервированные слова для CA1716. Метод стратегии называетсяPick.
Грабли, уже оплаченные
- Селектор типа в Avalonia матчит точный тип.
UserControl.shellне матчитShellView(наследникReactiveUserControl<T>) и молча не делает ничего. Использовать:is(UserControl).shell. Голый.shellтоже матчит, но тогда XAML-компилятор не может вывести тип дляSetterи падает с AVLN2200. - Style-сеттер навсегда проигрывает локальному значению. Не стилизовать
IsPaneOpenиDisplayMode— они биндятся во ViewModel. IPseudoClasses.Setтребует ведущего:.InvariantGlobalizationобязан бытьfalse: Semi.Avalonia строитCultureInfoв статическом конструкторе и падает целиком.- Превьюер рефлексирует безпараметровый статический
BuildAvaloniaApp(). Необязательный параметр ломает его вызов, вторая перегрузка —AmbiguousMatchException. - Compiled bindings включены по умолчанию (Avalonia 12):
x:DataTypeнужен на каждомUserControlи каждомDataTemplate. Avalonia.Headless.XUnit12.x — это xUnit v3, а не v2. Тестовые проекты —Exe.- Headless: ручной
Measure/Arrangeвнутри окна бесполезен — следующий проход layout окна вернёт свой размер. Задавать ширину самомуWindow. Но и без окна нельзя: у открепленного контрола не строится визуальное дерево. - csharpier — единственный владелец форматирования (включая
.axamlи.csproj).IDE0055понижен до suggestion: два форматтера сTreatWarningsAsErrorsдерутся насмерть. Condition="'$(Configuration)' == 'Debug'"наPackageReference— ловушка.dotnet restoreвычисляется с конфигурацией по умолчанию, поэтому пакет обязан резолвиться, даже если эту конфигурацию никто не собирает. Так тут проехал мёртвыйAvalonia.Diagnostics(его нет под Avalonia 12):dotnet build -c Releaseработал, а голыйdotnet restoreпадал.Execute()завершился ≠IsExecutingуже false. Второе публикуется на выходном планировщике. Тест, который сразу послеawaitдёргает команду, закрытую по чужомуIsExecuting, будет мигать под нагрузкой — ждитеCanExecute, а не предполагайте.- Проект VM-тестов не параллелится.
ReactiveUiBootstrapставит глобальные планировщики ReactiveUI, то есть тесты делят изменяемое состояние независимо от их желания. - Инспектора в Avalonia 12 нет из коробки.
Avalonia.Diagnosticsзакончился на 11.3.x; DevTools живут отдельно (AvaloniaUI.DiagnosticsSupport+.WithDeveloperTools()), со своей установкой. Зависимость намеренно не добавлена.
Качество
- Центральные версии пакетов —
Directory.Packages.props. НикакихVersion=в csproj (иначе NU1008 на restore). TreatWarningsAsErrorsвключён; NuGet-advisory (NU19xx) выведены из ошибок, чтобы свежая CVE не роняла сборку кода, который никто не трогал.- Тестовые послабления анализаторов — в
tests/Directory.Build.props, не в самих тестах.
Что осталось абстрактным
Домен — заглушка. IParser<TInput, TOutput> + DelimitedTextParser + KeyValueTextParser
существуют, чтобы каркас проверялся end-to-end. Когда появится настоящая доменная логика,
демо-парсеры удаляются вместе с их тестами и SampleFor/LargeSampleFor в ParseViewModel.