mostlylucid.ephemeral 1.3.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package mostlylucid.ephemeral --version 1.3.0
                    
NuGet\Install-Package mostlylucid.ephemeral -Version 1.3.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="mostlylucid.ephemeral" Version="1.3.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="mostlylucid.ephemeral" Version="1.3.0" />
                    
Directory.Packages.props
<PackageReference Include="mostlylucid.ephemeral" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add mostlylucid.ephemeral --version 1.3.0
                    
#r "nuget: mostlylucid.ephemeral, 1.3.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package mostlylucid.ephemeral@1.3.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=mostlylucid.ephemeral&version=1.3.0
                    
Install as a Cake Addin
#tool nuget:?package=mostlylucid.ephemeral&version=1.3.0
                    
Install as a Cake Tool

Mostlylucid.Ephemeral

NuGet License

Fire... and Don't Quite Forget.

Bounded, observable, self-cleaning async execution with signal-based coordination.

dotnet add package mostlylucid.ephemeral

Quick Start

using Mostlylucid.Ephemeral;

// One-shot parallel processing
await items.EphemeralForEachAsync(
    async (item, ct) => await ProcessAsync(item, ct),
    new EphemeralOptions { MaxConcurrency = 8 });

// Long-lived coordinator
await using var coordinator = new EphemeralWorkCoordinator<WorkItem>(
    async (item, ct) => await ProcessAsync(item, ct),
    new EphemeralOptions { MaxConcurrency = 8 });

await coordinator.EnqueueAsync(new WorkItem("data"));

// See what's happening
var running = coordinator.GetRunning();
var failed = coordinator.GetFailed();
var pending = coordinator.PendingCount;

// Graceful shutdown
coordinator.Complete();
await coordinator.DrainAsync();

All Options

new EphemeralOptions
{
    // ═══════════════════════════════════════════════════════════════
    // CONCURRENCY
    // ═══════════════════════════════════════════════════════════════

    // Max parallel operations overall
    // Default: Environment.ProcessorCount
    MaxConcurrency = 8,

    // Allow runtime concurrency adjustment via SetMaxConcurrency()
    // Default: false (fastest hot-path)
    EnableDynamicConcurrency = false,

    // Max parallel operations per key (keyed coordinators only)
    // Default: 1 (sequential per key)
    MaxConcurrencyPerKey = 1,

    // ═══════════════════════════════════════════════════════════════
    // MEMORY / WINDOW
    // ═══════════════════════════════════════════════════════════════

    // Max operations retained in memory (LRU eviction)
    // Default: 200
    MaxTrackedOperations = 200,

    // Max age for tracked operations before cleanup
    // Default: 5 minutes
    MaxOperationLifetime = TimeSpan.FromMinutes(5),

    // ═══════════════════════════════════════════════════════════════
    // FAIR SCHEDULING (keyed coordinators only)
    // ═══════════════════════════════════════════════════════════════

    // Prevent hot keys from starving cold keys
    // Default: false (FIFO ordering)
    EnableFairScheduling = false,

    // Pending count before a key is deprioritized
    // Default: 10
    FairSchedulingThreshold = 10,

    // ═══════════════════════════════════════════════════════════════
    // SIGNALS
    // ═══════════════════════════════════════════════════════════════

    // Shared signal sink across coordinators
    // Default: null (isolated)
    Signals = new SignalSink(),

    // Sync callback on signal raise (keep fast!)
    // Default: null
    OnSignal = evt => Console.WriteLine($"Signal: {evt.Signal}"),

    // Async callback on signal raise (background queue, non-blocking)
    // Default: null
    OnSignalAsync = async (evt, ct) => await LogToService(evt, ct),

    // Sync callback on signal retract
    // Default: null
    OnSignalRetracted = evt => Console.WriteLine($"Retracted: {evt.Signal}"),

    // Async callback on signal retract
    // Default: null
    OnSignalRetractedAsync = async (evt, ct) => await NotifyService(evt, ct),

    // Max concurrent async signal handlers
    // Default: 4
    MaxConcurrentSignalHandlers = 4,

    // Max queued signals before dropping oldest
    // Default: 1000
    MaxQueuedSignals = 1000,

    // Self-documenting: signals this coordinator may emit
    // Default: null (no enforcement)
    Emits = new[] { "started", "completed", "error" },

    // Self-documenting: signals this coordinator listens for
    // Default: null (no enforcement)
    Listens = new[] { "backpressure", "shutdown" },

    // Constraints for signal propagation (cycles, depth, terminals)
    // Default: null
    SignalConstraints = new SignalConstraints { MaxDepth = 10 },

    // ═══════════════════════════════════════════════════════════════
    // SIGNAL-BASED CONTROL FLOW
    // ═══════════════════════════════════════════════════════════════

    // Skip new items when these signals are present
    // Use for circuit-breaker patterns
    // Default: null
    CancelOnSignals = new HashSet<string> { "circuit-open", "shutdown" },

    // Delay new items when these signals are present
    // Use for backpressure patterns
    // Default: null
    DeferOnSignals = new HashSet<string> { "backpressure", "rate-limited" },

    // How often to recheck when deferring
    // Default: 100ms
    DeferCheckInterval = TimeSpan.FromMilliseconds(100),

    // Max defer attempts before running anyway
    // Default: 50 (5 seconds at 100ms)
    MaxDeferAttempts = 50,

    // ═══════════════════════════════════════════════════════════════
    // SAMPLING
    // ═══════════════════════════════════════════════════════════════

    // Callback after each operation with window snapshot
    // Runs on caller's thread - keep it cheap!
    // Default: null
    OnSample = snapshot => metrics.Record(snapshot.Count)
}

Coordinators

EphemeralWorkCoordinator<T>

Long-lived work queue with bounded concurrency.

await using var coordinator = new EphemeralWorkCoordinator<Request>(
    async (req, ct) => await HandleAsync(req, ct),
    new EphemeralOptions { MaxConcurrency = 8 });

// Enqueue work
await coordinator.EnqueueAsync(request);
var id = await coordinator.EnqueueWithIdAsync(request);  // Get operation ID

// Query state
var running = coordinator.GetRunning();
var completed = coordinator.GetCompleted();
var failed = coordinator.GetFailed();
var snapshot = coordinator.GetSnapshot();
var byId = coordinator.GetById(id);

// Stats
int pending = coordinator.PendingCount;
int active = coordinator.ActiveCount;
int totalCompleted = coordinator.TotalCompleted;
int totalFailed = coordinator.TotalFailed;

// Dynamic concurrency (requires EnableDynamicConcurrency = true)
coordinator.SetMaxConcurrency(16);
int current = coordinator.CurrentMaxConcurrency;

// Signals
bool hasError = coordinator.HasSignal("error");
int errorCount = coordinator.CountSignals("error");
var errors = coordinator.GetSignalsByPattern("error.*");
var recent = coordinator.GetSignalsSince(DateTimeOffset.UtcNow.AddMinutes(-1));

// Shutdown
coordinator.Complete();          // Stop accepting new work
await coordinator.DrainAsync();  // Wait for in-flight to finish

EphemeralKeyedWorkCoordinator<T, TKey>

Per-key sequential processing with optional fair scheduling.

await using var coordinator = new EphemeralKeyedWorkCoordinator<Order, string>(
    keySelector: order => order.CustomerId,
    body: async (order, ct) => await ProcessOrder(order, ct),
    new EphemeralOptions
    {
        MaxConcurrency = 16,          // Total parallel
        MaxConcurrencyPerKey = 1,     // Sequential per customer
        EnableFairScheduling = true   // Prevent hot customer starvation
    });

await coordinator.EnqueueAsync(order);

EphemeralResultCoordinator<TInput, TResult>

Capture results from async operations.

await using var coordinator = new EphemeralResultCoordinator<Request, Response>(
    async (req, ct) => await FetchAsync(req, ct),
    new EphemeralOptions { MaxConcurrency = 4 });

var id = await coordinator.EnqueueAsync(request);
var snapshot = await coordinator.WaitForResult(id);

if (snapshot.HasResult)
    Console.WriteLine(snapshot.Result);
else if (snapshot.Exception != null)
    Console.WriteLine($"Failed: {snapshot.Exception.Message}");

PriorityWorkCoordinator<T>

Multiple priority lanes.

var coordinator = new PriorityWorkCoordinator<WorkItem>(
    body: async (item, ct) => await ProcessAsync(item, ct),
    new PriorityWorkCoordinatorOptions<WorkItem>(
        Lanes: new[]
        {
            new PriorityLane("critical", MaxDepth: 100),
            new PriorityLane("high"),
            new PriorityLane("normal"),
            new PriorityLane("low")
        }
    ));

await coordinator.EnqueueAsync(item, "critical");

Extension Method

One-shot parallel processing of collections.

await items.EphemeralForEachAsync(
    async (item, ct) => await ProcessAsync(item, ct),
    new EphemeralOptions
    {
        MaxConcurrency = 8,
        OnSignal = evt => Console.WriteLine(evt.Signal)
    });

Signals

Emitting Signals

// From within operation body (via ISignalEmitter passed to body)
await coordinator.EnqueueAsync(request);  // Body receives emitter

// From shared sink
var sink = new SignalSink();
sink.Raise("backpressure.downstream");
sink.Raise(new SignalEvent("error.timeout", operationId, key, DateTimeOffset.UtcNow));

// Retract signals
sink.Retract("backpressure.downstream");

Querying Signals

// Exact match
bool hasError = coordinator.HasSignal("error");
int count = coordinator.CountSignals("error");

// Pattern matching (glob-style: * and ?)
var errors = coordinator.GetSignalsByPattern("error.*");
var httpErrors = coordinator.GetSignalsByPattern("http.error.*");

// Time-based
var recent = coordinator.GetSignalsSince(DateTimeOffset.UtcNow.AddSeconds(-30));

// From shared sink
var snapshot = sink.Sense();
var filtered = sink.Sense(s => s.Signal.StartsWith("error"));

Shared Signal Sink

var sink = new SignalSink(maxCapacity: 1000, maxAge: TimeSpan.FromMinutes(5));

var coordinator1 = new EphemeralWorkCoordinator<A>(body, new EphemeralOptions { Signals = sink });
var coordinator2 = new EphemeralWorkCoordinator<B>(body, new EphemeralOptions { Signals = sink });

// Both coordinators see signals raised by either
sink.Raise("system.maintenance");

Dependency Injection

// Register
services.AddEphemeralWorkCoordinator<WorkItem>(
    async (item, ct) => await ProcessAsync(item, ct),
    new EphemeralOptions { MaxConcurrency = 8 });

// Named coordinators
services.AddEphemeralWorkCoordinator<WorkItem>("priority",
    async (item, ct) => await ProcessPriorityAsync(item, ct),
    new EphemeralOptions { MaxConcurrency = 4 });

// Inject
public class MyService(IEphemeralCoordinatorFactory<WorkItem> factory)
{
    public async Task DoWork()
    {
        var coordinator = factory.CreateCoordinator();
        // or: factory.CreateCoordinator("priority")
        await coordinator.EnqueueAsync(new WorkItem());
    }
}

Package Description
mostlylucid.ephemeral.atoms.fixedwork Fixed worker pool with stats
mostlylucid.ephemeral.atoms.keyedsequential Per-key sequential processing
mostlylucid.ephemeral.atoms.signalaware Pause/cancel on signals
mostlylucid.ephemeral.atoms.batching Time/size batching
mostlylucid.ephemeral.atoms.retry Exponential backoff retry
mostlylucid.ephemeral.patterns.circuitbreaker Signal-based circuit breaker
mostlylucid.ephemeral.patterns.backpressure Signal-driven backpressure
mostlylucid.ephemeral.complete All packages in one DLL

Target Frameworks

.NET 6.0, 7.0, 8.0, 9.0, 10.0

License

Unlicense (public domain)

Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (44)

Showing the top 5 NuGet packages that depend on mostlylucid.ephemeral:

Package Downloads
mostlylucid.botdetection

Bot detection middleware for ASP.NET Core applications with behavioral analysis, header inspection, IP-based detection, and optional LLM-based classification.

Mostlylucid.Ephemeral.Atoms.KeyedSequential

Per-key sequential execution atom with global parallelism and optional fair scheduling.

Mostlylucid.Ephemeral.Atoms.SlidingCache

Caches work results with sliding expiration - accessing a result resets its TTL. Results that haven't been accessed expire and are recomputed on next request.

Mostlylucid.Ephemeral.Atoms.Taxonomy

Shared contracts, shards, and base types for Ephemeral taxonomy atoms (AtomKind, AtomContract, SignalDrivenAtom, MultiTaxonomyAtom).

mostlylucid.ephemeral.complete

Meta-package that references all Mostlylucid.Ephemeral packages - bounded async execution with signals, atoms, and patterns. Install this single package to get everything.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.4.0 941 4/17/2026
2.3.2 5,873 1/9/2026
2.3.1 1,261 1/9/2026
2.3.1-alpha0 995 1/9/2026
2.3.0 1,974 1/8/2026
2.3.0-alpha1 1,723 1/8/2026
2.1.0 1,028 1/8/2026
2.1.0-preview 996 1/8/2026
2.0.1 1,005 1/8/2026
2.0.0 1,043 1/8/2026
2.0.0-alpha1 1,330 1/8/2026
1.7.1 1,396 12/11/2025
1.6.8 1,254 12/9/2025
1.6.7 1,237 12/9/2025
1.6.6 1,227 12/9/2025
1.6.5 1,225 12/9/2025
1.6.0 1,215 12/8/2025
1.5.0 1,137 12/8/2025
1.3.0 968 12/7/2025
1.2.2 953 12/7/2025
Loading failed

v1.0.0
- Initial release
- EphemeralWorkCoordinator for bounded async execution
- EphemeralKeyedWorkCoordinator for per-key ordering
- PriorityWorkCoordinator for priority lanes
- Signal-based coordination (SignalSink, SignalDispatcher)
- ParallelEphemeral extension methods
- DI integration