wan24-Core
1.18.2
See the version list below for details.
dotnet add package wan24-Core --version 1.18.2
NuGet\Install-Package wan24-Core -Version 1.18.2
<PackageReference Include="wan24-Core" Version="1.18.2" />
paket add wan24-Core --version 1.18.2
#r "nuget: wan24-Core, 1.18.2"
// Install wan24-Core as a Cake Addin #addin nuget:?package=wan24-Core&version=1.18.2 // Install wan24-Core as a Cake Tool #tool nuget:?package=wan24-Core&version=1.18.2
wan24-Core
This core library contains some .NET extensions:
- Bootstrapping
- Disposable base class for disposable types, which supports asynchronous
disposing
- Dispose attribute for fields/properties which should be disposed automatic when disposing
CancellationOnDispose
cancels a cancellation token when an object is being disposed (or another given cancellation token ws canceled)Cancellations
combines multiple cancellation tokens into one- Type helper (type loading)
- Secure byte and char array, which clears its contents when disposing
- Pool rented array as disposable object (which optionally clears its contents
when disposing; for byte/char arrays just like the
Secure*Array
) - Byte array extensions
- Endian conversion
- Bit-converter (endian-safe)
- UTF-8/16/32 (little endian) string decoding
- Clearing
- Char array extensions
- Clearing
- Array helper extensions
- Offset/length validation
- Array pool extensions
- Renting a cleared array
- Enumerable extensions
- Combine enumerables
- Chunk enumerables
- Reflection extensions
- Automatic parameter extension when invoking a method (with DI support)
- Synchronous/asynchronous method invokation
- Automatic constructor invokation using a given parameter set (with DI support)
- Nullability detection
- Delegate extensions
- Delegate list invokation (with or without return values, with DI support)
- Asynchronous delegate list invokation (with or without return values, with DI support)
- Task extensions
- Result getting of a generic task
- Asynchronous task list awaiting
- Shortcuts for await configurations
- Shortcuts for starting a function as long running task
- Shortcuts for starting a function as task with fair execution by the scheduler
- Add a cancellation token to a task (which can cancel the task awaiter)
- DI helper
- Service provider adoption
- DI object factory delegates
- Asynchronous DI object factory delegates
- Enumeration extensions
- Get enumeration value display string from
DisplayTextAttribute
or usingToString
(fallback) - Determine if all, any or which flags are contained in an enumeration value
- Remove flags of a mixed enumeration value
- Get only flags of a mixed enumeration value
- Value validation
- Get enumeration value display string from
- Number extensions
- Determine if a type is a number
- Determine if a number type is unsigned
- Bit-converter (endian-safe)
- Determine if a number (or any
IComparable
) is within a range
- Numeric bitwise extensions
- Collection extensions
- Add a range of items
- JSON helper
- Exchangeable JSON encoder/decoder delegates (using
System.Text.Json
per default)
- Exchangeable JSON encoder/decoder delegates (using
- JSON extensions
- Encode an object
- Decode from a type
- Decode a string
- Object extensions
- Type conversion
- Determine if a value is within a list of values
- String extensions
- Get UTF-8/16/32 bytes (little endian)
- Generic helper
- Determine if two generic values are equal
- Determine if a value is
null
- Determine if a value is
default
- Determine if a value is
null
ordefault
DateTime
extensions- Determine if a time is within a range
- Determine if a time matches a reference time plus/minus an offset
- Apply an offset to a time base on a reference time
TimeSpanHelper
- Update a timeout
- Queue worker (for actions and/or items)
- Parallel queue worker (for actions and/or items)
ParallelAsync
implementationForEachAsync
with an asynchronous or synchronous input sourceFilterAsync
with an asynchronous or synchronous input source and item filterFilter
for synchronous parallel filtering
- Base class for a hosted worker, which implements the
IHostedService
interface (timed or permanent running) EventThrottle
for throttling event handler callsProcessThrottle
for throttling a processing channelOrderedDictionary<tKey, tValue>
is used for working with indexed key/value pairsTimeout
will count down and raise an event, if not reset before reaching the timeoutILogger
supportIChangeToken
support usingChangeCallback
- Hierarchic configuration using
OverrideableConfig
- Cancellation token awaiter
ObjectPool
for pooling objects (DisposableObjectPool
for disposable types), andBlockingObjectPool
for a strict pool capacity limitResetEvent
for (a)synchronous event waitingLazyValue<T>
,DisposableLazyValue<T>
,AsyncDisposableLazyValue<T>
andTimeoutValue<T>
for lazy and timeout value servingObjectLockManager<T>
for asynchronous and awaitable object lockingBitmap
for working with bitsDisposableWrapper<T>
for wrapping any (not disposable?) object with theIDisposable
andIAsyncDisposable
interface using custom dispose actions during runtimeDisposableAdapter
for adopting theIDisposableObject
interface from a type which can't extend theDisposableBase
type- Generic object extenions for validating method arguments
- CLI arguments interpreter
How to get it
This library is available as NuGet package "wan24-Core".
Bootstrapping
The Bootstrapper.Async
method calls all static methods having the
BootstrapperAttribute
. In order to be able to find the methods, it's
required to add the BootstrapperAttribute
to the assembly.
You may also ad the BootstrapperAttribute
to a type and/or the bootstrapper
method, in case the assembly contains multiple of them. In the assembly
attribute you need to set ScanClasses
and/or ScanMethods
to true
in
order to perform a deep scanning during bootstrapping for performance reasons.
The bootstrapper methods may consume parameters which are available from the DI helper. The method may be synchronous or asynchronous. The method can't be defined in a generic class, and it can't be generic itself.
[assembly:Bootstrapper(typeof(YourBootstrapper),nameof(YourBootstrapper.BootstrapperMethod))]
public static class YourBootstrapper
{
public static async Task BootstrapperMethod()
{
// Perform your bootstrapping here
}
}
// Call the bootstrapper somewhere in your apps initialization code
await Bootstrap.Async();
The BootstrapperAttribute
can be initialized with a numeric priority. The
bootstrapper will order the found bootstrapping methods by priority, where the
one with the highest number will be executed first (assembly and type
priorities count, too). At last there's a assembly location, type and method
name sorting. Bootstrapper methods will be executed sequential.
If you give a type and a method name to the assembly BootstrapperAttribute
,
you won't need to add the attribute to the type and the method.
During bootstrapping, the cancellation token which was given to the
Bootstrap.Async
method, can be injected to a bootstrappers method parameters.
After that bootstrapping was done, the Bootstrap.AsyncBootstrapper
will be
called. At last the Bootstrap.OnBootstrap
event will be raised.
During bootstrapping the Bootstrap.IsBooting
property is true
. After
bootstrapping the Bootstrap.DidBoot
property is true
.
The bootstrapper will load all referenced assemblies. If you load an assembly
later, it'll be bootstrapped automatic and added to the TypeHelper
singleton
instance.
Type helper
If you use the TypeHelper.AddTypes
method, the unknown assemblies of the
added types will be added as searchable assemblies automatic.
You may attach to the TypeHelper.OnLoadType
event for handling requests
more dynamic.
The TypeHelper.GetType
method will try Type.GetType
first and fall back to
the helper, if no type was found.
DI helper
In order to make DI (dependency injection) working, you need to
- set a
DiHelper.ServiceProvider
and/or - add
DiHelper.(Async)ObjectFactories
The DiHelper.GetDiObjectAsync
method will try to resolve the request
synchronous, first. But the DiHelper.GetDiObject
won't try asynchronous
object factories.
Mixed enumeration value
A mixed enumeration contains X bits enumeration values, and Y bits flags:
[Flags]
public enum MixedEnum : int
{
None = 0,
Value1 = 1,
Value2 = 2,
Value3 = 3,
...
Flag1 = 1 << 8,
Flag2 = 1 << 9,
FLAGS = Flag1 | Flag2 // Required to identify flags
}
The FLAGS
value helps these extension methods to handle flag values:
MixedEnum value = MixedEnum.Value1 | MixedEnum.Flag1,
valueOnly = value.RemoveFlags(),// == MixedEnum.Value1
flagsOnly = value.OnlyFlags();// == MixedEnum.Flag1
Unsafe code
The library uses unsafe code. If you don't want/need that, you can compile the
library with the NO_UNSAFE
compiler constant to disable any unsafe
operation. Remember to unset the unsafe compiler option, too!
Disposable base class
The DisposableBase
implements the IDisposable
and IAsyncDisposable
interfaces. It provides some helpers and events, and also the
DisposeAttribute
, which can be applied to fields and properties which you
wish to dispose automatic when disposing.
When your type derives from the DisposableBase
, you'll need to implement the
abstract Dispose
method:
protected override Dispose(bool disposing)
{
// Your dispose logic here
}
There are measures to avoid that this method is being called twice.
To implement custom asynchronous disposing:
protected override async Task DisposeCore()
{
// Your dispose logic here
}
In order to make the DisposeAttribute
working, you have to call the
protected method DisposeAttributes
or DisposeAttributesAsync
.
The IsDisposing
property value will be true
as soon as the disposing
process started, and it will never become false
again. The IsDisposed
property value will be true
as soon as the disposing process did finish.
Queue worker
using QueueWorker worker = new();
await worker.EnqueueAsync((ct) =>
{
// Do any background action here
});
The QueueWorker
class can be extended as you need it.
The ParallelQueueWorker
requires a number of threads in the constructor,
which defines the degree of parallelism, in which enqueued tasks will be
processed.
Queue item worker
using QueueItemWorker<ItemType> worker = new();
await worker.EnqueueAsync(new ItemType());
The QueueItemWorker<T>
class can be extended as you need it.
The ParallelItemQueueWorker<T>
requires a number of threads in the
constructor, which defines the degree of parallelism, in which enqueued items
will be processed.
ParallelAsync
Using the .NET parallel implementation it's not possible to invoke
asynchronous item handlers. For this you can use the
ParallelAsync.ForEachAsync
method, which uses a parallel item queue worker
in the background for asynchronous processing.
Hosted worker
public class YourHostedWorker : HostedWorkerBase
{
public YourHostedWorker() : base() { }
protected override async Task WorkerAsync()
{
// Perform the service actions here
}
}
The hosted worker implements the IHostedService
interface and can be
extended as you need it.
Timed hosted worker
public class YourHostedWorker : TimedHostedWorkerBase
{
public YourHostedWorker() : base(interval: 500) { }
protected override async Task WorkerAsync()
{
// Perform the service actions here
}
}
This example uses a 500ms timer. Based on the defined timer type, the interval will be processed in different ways:
Default
: Next worker run is now plus the interval (used by default)Exact
: Next worker run is now plus the interval minus the processing duration (used, if the start time of the processing is important)ExactCatchingUp
: AsExact
, but catching up missing processing runs without delay, if a worker run duration exceeds the interval (used, if the number of worker runs is important)
Using the SetTimerAsync
method you can change the timer settings at any
time. If you give the nextRun
parameter, you may set a fixed next run time
(which won't effect the given interval, but just force the service to run at a
specific time for the next time).
NOTE: The nextRun
parameter will also force the service to (re)start!
By setting the property RunOnce
to true
, the service will stop after
running the worker once. In combination with the SetTimerAsync
parameter
nextRun
you can execute the worker at a specific time once.
The hosted worker implements the IHostedService
interface and can be
extended as you need it.
EventThrottle
public class YourType : DisposableBase
{
protected readonly YourEventThrottle EventThrottle;
public YourType() : base() => EventThrottle = new(this);
// This method will raise the OnEvent
public void AnyMethod()
{
RaiseOnEventThrottled();
}
protected override Dispose(bool disposing) => EventThrottle.Dispose();
// Delegate for OnEvent
public delegate void YourTypeEvent_Delegate();
// Event to throttle
public event YourTypeEvent_Delegate? OnEvent;
// Raise the OnEvent using the event throttle
protected void RaiseOnEventThrottled() => EventThrottle.Raise();
// Finally let the event handlers process the event
protected void RaiseOnEvent() => OnEvent?.Invoke();
// Event throttle implementation
public class YourEventThrottle : EventThrottle
{
// Throttle the event handling down to max. one handling per 300ms
public YourEventThrottle(YourType instance) : base(timeout: 300) => Instance = instance;
public YourType Instance { get; }
protected override HandleEvent(DateTime raised, int raisedCount)
{
Instance.RaiseOnEvent();
}
}
}
If AnyMethod
is being called, the event will be forwarded to the event
throttle, which decides to throttle or raise the event. If AnyMethod
was
called three times within 300ms, the first call will be executed in realtime,
while the 2nd and the 3rd call will be sqashed and executed once 300ms after
the 1st call was processed.
This example assumes you're working with a real event - but you may throttle any event (which may not be a real event) using throttling logic.
ProcessThrottle
public class YourProcessThrottle : ProcessThrottle
{
// Throttle to processing one object per second
public YourProcessThrottle() : base(limit: 1, timeout: 1000) { }
// Processing API using a timeout
public async Task<int> ProcessAsync(Memory<bool> items, TimeSpan timeout)
=> await ProcessAsync(items.Length, (count) =>
{
await Task.Yield();
Span<bool> toProcess = items.Span[..count];
items = items[count..];
// Process toProcess
}, timeout);
// Processing API using a cancellation token
public async Task<int> ProcessAsync(Memory<bool> items, CancellationToken token = default)
=> await ProcessAsync(items.Length, (count) =>
{
await Task.Yield();
Span<bool> toProcess = items.Span[..count];
items = items[count..];
// Process toProcess
}, token);
}
The example will throttle the processing to a maximum of one object per
second. Multiple threads may call ProcessAsync
concurrent - processing will
be organized thread-safe.
The return value of ProcessAsync
is the number of objects processed until
timeout or canceled.
The processing delegate shouldn't care about the timeout or if canceled and just process the given number of objects.
NOTE: A usage gap will slide the throttling timer. Example:
The timeout was set to 3 objects per 100ms. Now processing goes like this:
- First processed object on
0ms
will activate the throttling timeout - Next processed object on
10ms
will increase the object throttling counter - Next processed object on
110ms
will reset the throttling timeout and counter (the usage gap of 100ms does exceed the timeout) - Next 2 processed objects on
120ms
will activate the throttle - Next object will have to wait until the throttle was released
- The throttle will be released on
210ms
, which allows the last object to be processed now
In short words: The throttle timer will not reset in an fixed interval, but the interval starts when processing items.
Change token
Implement by extending ChangeToken
:
public class YourObservableType : ChangeToken
{
public YourObservableType() : base()
{
ChangeIdentifier = () => HasChanged;
}
public bool HasChanged => ...;// Return if the object was changed
public void ChangeAction()
{
// Perform changes
InvokeCallbacks();
}
}
Or by using a ChangeToken
instance:
public class YourObservableType : IChangeToken
{
public readonly ChangeToken ChangeToken;
public YourObservableType() => ChangeToken = new(() => HasChanged);
public bool HasChanged => ...;// Return if the object was changed
public void ChangeAction()
{
// Perform changes
ChangeToken.InvokeCallbacks();
}
// Implement the IChangeToken interface using our ChangeToken instance
bool IChangeToken.HasChanged => ChangeToken.HasChanged;
bool IChangeToken.ActiveChangeCallbacks => ChangeToken.ActiveChangeCallbacks;
IDisposable IChangeToken.RegisterChangeCallback(Action<object?> callback, object? state)
=> ChangeToken.RegisterChangeCallback(callback, state);
}
Hierarchic configuration
Assume this configuration hierarchy:
Level | Description |
---|---|
1 | Default values |
2 | User values (can override default values) |
3 | Administrator values (can override default/user values) |
In code:
public sealed class Config : OverrideableConfig<Config>
{
public Config() : base()
{
SubConfig = new(this, new(this));// User values
InitProperties();
}
private Config(Config parent, Config? sub = null) : base(parent)
{
if(sub != null)
{
SubConfig = sub;
sub.ParentConfig = this;
sub.SubConfig = new(sub);// Administrator values
}
InitProperties();
}
// A configuration value
public ConfigOption<string, Config> AnyValue { get; private set; } = null!;
private void InitProperties()
{
AnyValue = ParentConfig == null
// The master option has a default value
? new(this, nameof(AnyValue), canBeOverridden: true, "default")
// No default value for a sub-option
: new(this, nameof(AnyValue));
}
}
Config config = new(),
user = config.SubConfig,
admin = user.SubConfig;
CAUTION: There's no endless-recursion protection for the ParentConfig
or
the SubConfig
properties!
Now users are able to override default values, and administrators are able to override default and/or user values:
// Still the default value
Assert.AreEqual("default", config.AnyValue.FinalValue);
// User overrides the default value
user.AnyValue.Value = "user";
Assert.AreEqual("default", config.AnyValue.Value);
Assert.AreEqual("user", config.AnyValue.FinalValue);
// Administrator overrides the user value
admin.AnyValue.Value = "admin";
Assert.AreEqual("admin", config.AnyValue.FinalValue);
// User can't override the administrator value (but still store his own value
// in case the administrator would unset his value)
user.AnyValue.Value = "test";
Assert.AreEqual("admin", config.AnyValue.FinalValue);
Assert.AreEqual("test", user.AnyValue.Value);
NOTE: Setting an option value is thread-safe.
It's also possible to flip the hierarchy:
Level | Description |
---|---|
1 | Default values |
2 | Administrator values (can define user visible and optional not overrideable values) |
3 | User values (can override overrideable values) |
Using this hierarchy an administrator could also allow or deny overriding values at any time, for example.
The hierarchy depth isn't limited.
Object locking
The ObjectLockManager<T>
helps locking any object during an asynchronous
operation:
ObjectLock ol = await ObjectLockManager<AnyType>.Shared.LockAsync(anyObjectKey);
// A 2nd call to ObjectLockManager<AnyType>.Shared.LockAsync would block until the lock was released
await ol.RunTaskAsync(Task.Run(async () =>
{
// Perform the asynchronous operation here
}));
// ol is disposed already, 'cause the asynchronous operation source task was awaited
// The next ObjectLockManager<AnyType>.Shared.LockAsync call will be processed now, if any
await ol.Task;// To throw any exception during performing the asynchronous operation
If AnyType
implements the IObjectKey
interface, it can be given to the
ObjectLockManager<T>
methods as object argument.
NOTE: ObjectLock
will dispose itself as soon as RunTaskAsync
has been
called, and the given task was completed.
CLI arguments interpreter
There a just a few rules:
- A flag starts with a single dash
- A key for a value (list) starts with a double dash
- Keys/values can be quoted using single or double quotes
- Escape character is the backslash (only applicable in quoted values)
- A quoted value must be escaped for JSON decoding, a backslash must be double escaped
- Double quotes in a quoted value must be escaped always
Example:
"-flag" --key 'value1' value2 --key -value3 '--key2' "value"
For appending the value -value3
to the value list of key
, the value needs
to be added with another --key
key identifier, 'cause it starts with a dash
and could be misinterpreted as a flag (which would result in a parser error).
A CLI app called with these arguments could interpret them easy using the
CliArguments
class:
CliArguments cliArgs = new(args);
Assert.IsTrue(cliArgs["flag"]);
Assert.AreEqual(3, cliArgs.All("key").Count);
Assert.AreEqual("value", cliArgs.Single("key2"));
A --
(double dash) may be interpreted as an empty key name or a flag with
the name -
, based on if a value, which doesn't start with a dash, is
following. Examples:
--
:-
flag-- -
:-
flag (--
and-
are both interpreted as double-
flag (double flags will be combined))-- value
: Empty key with the valuevalue
-- -key
:-
andkey
flags
Keyless arguments will be stored in the KeyLessArguments
list - example:
CliArguments ca = CliArguments.Parse("value1 -flag value2 --key value3");
Assert.AreEqual(2, ca.KeyLessArguments.Count);
Assert.AreEqual("value1", ca.KeyLessArguments[0]);
Assert.AreEqual("value2", ca.KeyLessArguments[1]);
Assert.IsTrue(ca["flag"]);
Assert.IsTrue(ca["key", true]);
Product | Versions 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 was computed. 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 was computed. 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. |
-
net6.0
- Microsoft.Extensions.Hosting.Abstractions (>= 7.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 7.0.0)
NuGet packages (14)
Showing the top 5 NuGet packages that depend on wan24-Core:
Package | Downloads |
---|---|
Stream-Serializer-Extensions
Serializer extensions for .NET Stream objects. |
|
wan24-Compression
Compression helper |
|
wan24-Crypto
Crypto helper |
|
wan24-Crypto-BC
Bouncy Castle adoption to wan24-Crypto |
|
wan24-Compression-LZ4
LZ4 adoption for wan24-Compression |
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated |
---|---|---|
2.44.0 | 92 | 11/10/2024 |
2.43.0 | 88 | 11/3/2024 |
2.42.0 | 336 | 10/27/2024 |
2.41.0 | 76 | 10/21/2024 |
2.40.0 | 84 | 10/20/2024 |
2.39.0 | 97 | 9/29/2024 |
2.38.0 | 599 | 9/21/2024 |
2.37.0 | 114 | 9/15/2024 |
2.36.0 | 285 | 9/8/2024 |
2.35.0 | 227 | 8/24/2024 |
2.34.0 | 624 | 8/16/2024 |
2.33.0 | 230 | 8/4/2024 |
2.32.0 | 414 | 7/13/2024 |
2.31.0 | 390 | 7/6/2024 |
2.30.0 | 182 | 6/29/2024 |
2.29.0 | 349 | 6/22/2024 |
2.28.0 | 324 | 6/15/2024 |
2.27.0 | 97 | 6/8/2024 |
2.26.0 | 120 | 6/1/2024 |
2.25.0 | 127 | 5/26/2024 |
2.24.0 | 133 | 5/20/2024 |
2.23.0 | 172 | 5/11/2024 |
2.22.0 | 335 | 5/9/2024 |
2.21.0 | 139 | 5/5/2024 |
2.20.0 | 159 | 4/28/2024 |
2.19.0 | 157 | 4/20/2024 |
2.18.1 | 158 | 4/14/2024 |
2.18.0 | 344 | 4/12/2024 |
2.17.0 | 123 | 4/7/2024 |
2.16.0 | 215 | 3/30/2024 |
2.15.1 | 121 | 3/30/2024 |
2.15.0 | 112 | 3/30/2024 |
2.14.0 | 136 | 3/24/2024 |
2.13.0 | 148 | 3/17/2024 |
2.12.0 | 189 | 3/15/2024 |
2.11.0 | 153 | 3/10/2024 |
2.10.1 | 125 | 3/10/2024 |
2.10.0 | 229 | 3/9/2024 |
2.9.2 | 292 | 3/2/2024 |
2.9.1 | 132 | 3/2/2024 |
2.9.0 | 165 | 3/2/2024 |
2.8.0 | 138 | 2/25/2024 |
2.7.1 | 127 | 2/25/2024 |
2.7.0 | 111 | 2/25/2024 |
2.6.0 | 274 | 2/24/2024 |
2.5.0 | 117 | 2/20/2024 |
2.4.0 | 122 | 2/18/2024 |
2.3.2 | 186 | 2/17/2024 |
2.3.1 | 123 | 2/17/2024 |
2.3.0 | 121 | 2/17/2024 |
2.2.0 | 399 | 1/20/2024 |
2.1.0 | 125 | 12/23/2023 |
2.0.0 | 200 | 12/17/2023 |
1.43.0 | 161 | 11/27/2023 |
1.42.0 | 295 | 11/11/2023 |
1.41.2 | 119 | 11/4/2023 |
1.41.1 | 116 | 11/4/2023 |
1.41.0 | 118 | 11/4/2023 |
1.40.0 | 264 | 10/29/2023 |
1.39.0 | 276 | 10/21/2023 |
1.38.2 | 147 | 10/15/2023 |
1.38.1 | 317 | 10/14/2023 |
1.38.0 | 136 | 10/14/2023 |
1.37.0 | 138 | 10/13/2023 |
1.36.0 | 324 | 10/7/2023 |
1.35.0 | 212 | 10/1/2023 |
1.34.0 | 201 | 9/27/2023 |
1.33.0 | 124 | 9/20/2023 |
1.32.1 | 309 | 9/19/2023 |
1.32.0 | 114 | 9/19/2023 |
1.31.1 | 170 | 9/16/2023 |
1.31.0 | 185 | 9/16/2023 |
1.30.1 | 271 | 9/10/2023 |
1.30.0 | 136 | 9/10/2023 |
1.29.0 | 320 | 9/3/2023 |
1.28.0 | 141 | 8/26/2023 |
1.27.0 | 143 | 8/19/2023 |
1.26.0 | 163 | 8/5/2023 |
1.25.1 | 273 | 7/30/2023 |
1.25.0 | 263 | 7/30/2023 |
1.24.0 | 387 | 7/22/2023 |
1.23.0 | 136 | 7/9/2023 |
1.22.0 | 141 | 6/25/2023 |
1.21.0 | 162 | 6/24/2023 |
1.20.0 | 159 | 6/17/2023 |
1.19.0 | 204 | 6/11/2023 |
1.18.2 | 157 | 6/10/2023 |
1.18.1 | 157 | 6/9/2023 |
1.18.0 | 346 | 6/8/2023 |
1.17.0 | 144 | 6/4/2023 |
1.16.0 | 488 | 6/3/2023 |
1.15.0 | 317 | 5/29/2023 |
1.14.0 | 150 | 5/29/2023 |
1.13.0 | 152 | 5/28/2023 |
1.12.0 | 323 | 5/27/2023 |
1.11.0 | 143 | 5/24/2023 |
1.10.0 | 144 | 5/23/2023 |
1.9.0 | 133 | 5/22/2023 |
1.8.2 | 316 | 5/20/2023 |
1.8.1 | 156 | 5/20/2023 |
1.8.0 | 150 | 5/20/2023 |
1.7.1 | 171 | 5/13/2023 |
1.7.0 | 206 | 5/11/2023 |
1.6.1 | 1,519 | 4/26/2023 |
1.6.0 | 390 | 4/25/2023 |
1.5.0 | 418 | 4/22/2023 |
1.4.0 | 165 | 4/22/2023 |
1.3.0 | 303 | 4/16/2023 |
1.2.0 | 248 | 4/10/2023 |
1.1.0 | 188 | 4/7/2023 |
1.0.1 | 203 | 4/1/2023 |