Skip to content

Commit 1f41055

Browse files
committed
docs: add CLAUDE.md and fix agents.md symlink case
agents.md pointed at claude.md, which dangles on case-sensitive filesystems now the file is CLAUDE.md.
1 parent b36257c commit 1f41055

2 files changed

Lines changed: 62 additions & 1 deletion

File tree

CLAUDE.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Repository Overview
6+
7+
EnumerableAsyncProcessor is a NuGet library for processing asynchronous tasks with controlled concurrency: one at a time, batched, rate limited, timed rate limited (e.g. requests-per-second), or fully parallel. The library multi-targets `net8.0`, `net9.0`, and `net10.0` and is strong-named (`Directory.Build.props` signs with `strongname.snk`; internals are visible to the test project).
8+
9+
`agents.md` is a symlink to this file (`claude.md` resolves to `CLAUDE.md` on Windows' case-insensitive filesystem).
10+
11+
## Commands
12+
13+
```powershell
14+
# Build
15+
dotnet build
16+
17+
# Run all tests (TUnit on Microsoft.Testing.Platform; global.json wires dotnet test to MTP)
18+
dotnet test
19+
20+
# Run tests for a single target framework directly
21+
dotnet run --project EnumerableAsyncProcessor.UnitTests -f net10.0
22+
23+
# Run a single test — TUnit uses --treenode-filter (/Assembly/Namespace/Class/Method), NOT --filter
24+
dotnet run --project EnumerableAsyncProcessor.UnitTests -f net10.0 -- --treenode-filter "/*/*/*/TestMethodName"
25+
26+
# Run all tests in one class
27+
dotnet run --project EnumerableAsyncProcessor.UnitTests -f net10.0 -- --treenode-filter "/*/*/ParallelAsyncProcessorTests/*"
28+
```
29+
30+
TUnit test projects compile to executables; VSTest-style `dotnet test --filter` does not work. See the `tunit-testing` skill for full filter syntax.
31+
32+
CI (`.github/workflows/dotnet.yml`) runs the `EnumerableAsyncProcessor.Pipeline` project (a ModularPipelines app, `dotnet run -c Release` from that directory), which builds, tests, packs, and — on `main` — publishes to NuGet. Versioning comes from GitVersion (`GitVersion.yml` pins `next-version: 4.0.0`; keep that file present, its absence makes ModularPipelines generate a Mainline config that crashes on GitHub PR merge commits).
33+
34+
## Architecture
35+
36+
### Fluent API flow: extensions → builders → runnable processors
37+
38+
1. **Entry points** (`Extensions/EnumerableExtensions.cs`, `Extensions/AsyncEnumerableExtensions.cs`, `Builders/AsyncProcessorBuilder.cs`): `items.SelectAsync(...)` / `items.ForEachAsync(...)`, or `AsyncProcessorBuilder.WithItems(...)` / `.WithExecutionCount(n)` for source-less runs.
39+
2. **Builders** (`Builders/`) capture the items, the delegate, and a `CancellationTokenSource` linked to the caller's token.
40+
3. **Terminal methods** (`ProcessInParallel`, `ProcessInBatches`, `ProcessOneAtATime`) construct the matching processor from `RunnableProcessors/` and immediately call `StartProcessing()`**processing begins at build time**, not on first await.
41+
42+
### Processor matrix
43+
44+
Processor classes vary along three axes, reflected in naming:
45+
46+
- **Input**: with items (`<TInput>`) vs. execution-count only (non-generic).
47+
- **Output**: `Result*`-prefixed classes (in `RunnableProcessors/ResultProcessors/`) return values via `IAsyncProcessor<TOutput>` (`GetResultsAsync()`, `GetResultsAsyncEnumerable()`, `GetEnumerableTasks()`); unprefixed classes are fire-and-await (`WaitAsync()`).
48+
- **Strategy**: `OneAtATime`, `Batch`, `Parallel`, `RateLimitedParallel`, `TimedRateLimitedParallel`.
49+
50+
`RunnableProcessors/AsyncEnumerable/` holds parallel variants for `IAsyncEnumerable<T>` sources. File-name suffixes `_1`/`_2` distinguish generic arity (e.g. `BatchAsyncProcessor_1.cs` is `BatchAsyncProcessor<TInput>`).
51+
52+
### Core mechanics (read these before changing behavior)
53+
54+
- **`ProcessorLifecycle.cs`**: owns start/cancel/dispose shared by both base-class hierarchies. `AbstractAsyncProcessorBase` (void) and `ResultAbstractAsyncProcessorBase` (results) cannot share an ancestor because they fan out to differently typed `TaskCompletionSource` lists, so both delegate to this class. Cancellation is registered in `Start`, not the constructor, so a pre-cancelled token can never fire on a partially built instance. `DisposeAsync` waits up to 30 seconds for in-flight tasks; sync `Dispose` cancels without blocking.
55+
- **TCS-per-item**: each item gets a `TaskCompletionSource`; `TaskWrapper.Process` never throws — it completes the item's TCS with the failure/cancellation instead, so one failed item cannot kill the run or leave awaiters hanging.
56+
- **`WorkerPool.cs`**: rate-limited processors run a fixed pool of worker loops claiming items via `Interlocked.Increment` (P `Task.Run` tasks total, not N throttled tasks + semaphore). `minimumIterationTime` is how timed rate limiting is implemented: each worker holds its slot for at least that duration per item.
57+
- **Multi-targeting**: `EnumerableExtensions.ToIAsyncEnumerable` uses `Task.WhenEach` on `NET9_0_OR_GREATER` and a completion-order-bucket fallback otherwise. The test project targets `net8.0` specifically to exercise the fallback path — don't drop that TFM.
58+
59+
### Disposal contract
60+
61+
All processors implement `IDisposable`/`IAsyncDisposable`; the README documents the patterns users rely on (`await using`, safe double/early disposal). The builder extension shortcuts on `IAsyncEnumerable<T>` dispose internally; processors returned from the builder pattern are the caller's responsibility. Preserve these semantics — there are dedicated regression tests (`DisposalRegressionTests`, `ExceptionFidelityTests`, `InputEnumerationRegressionTests`).

agents.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
claude.md
1+
CLAUDE.md

0 commit comments

Comments
 (0)