Brainix.GoalsCommon
1.0.2-beta-145
See the version list below for details.
dotnet add package Brainix.GoalsCommon --version 1.0.2-beta-145
NuGet\Install-Package Brainix.GoalsCommon -Version 1.0.2-beta-145
<PackageReference Include="Brainix.GoalsCommon" Version="1.0.2-beta-145" />
paket add Brainix.GoalsCommon --version 1.0.2-beta-145
#r "nuget: Brainix.GoalsCommon, 1.0.2-beta-145"
// Install Brainix.GoalsCommon as a Cake Addin #addin nuget:?package=Brainix.GoalsCommon&version=1.0.2-beta-145&prerelease // Install Brainix.GoalsCommon as a Cake Tool #tool nuget:?package=Brainix.GoalsCommon&version=1.0.2-beta-145&prerelease
Documentation
Summary
The GoalsCommon Package is a package that consists of three main parts: a RabbitMQ bus, the learning tree structure and a DataLoad helper.
RabbitMQ
Dependencies
The RabbitMQ bus has the following dependencies:
Installation
To install the GoalsCommon Package, the following command must be executed in the project's folder:
dotnet add package Brainix.GoalsCommon --prerelease
Initialization of the RabbitMQ bus
The RabbitMQ bus is to be controlled via the Brainix.GoalsCommon.RabbitMQ.Bus.IBrainixEventBus
interface. There are two implementations for this in the Goals-Common package. The Brainix.GoalsCommon.RabbitMQ.Bus.DefaultRabbitMQBus
class is used for normal communication with a RabbitMQ server. The Brainix.GoalsCommon.RabbitMQ.Bus.TestRabbitMQBus
class was written to efficiently create integration tests with RabbitMQ.
DefaultRabbitMQBus
The DefaultRabbitMQBus is initialised with a RabbitMQConfiguration. This contains the complete ConnectionString
and an ApplicationName
. A normal initialisation would look like this:
using Brainix.GoalsCommon.RabbitMQ.Bus;
...
var brainixEventBus = new DefaultRabbitMQBus(new RabbitMQConfiguration(
"amqp://username:password@url:port/",
"serviceName"
));
In a real use case, the values for the ConnectionString
and the ApplicationName
would of course have to be chosen appropriately.
A classic initialisation in the context of a dependency injection would look like this:
using Brainix.GoalsCommon.RabbitMQ.Bus;
...
services.AddSingleton<IBrainixEventBus, DefaultRabbitMQBus>(conf => new DefaultRabbitMQBus(
new RabbitMQConfiguration(
Configuration.GetConnectionString("rabbit_mq").Replace(<%password%>",vault.rabbitMQPassword),
"TrainingCenter"
)
));
TestRabbitMQBus
The TestRabbitMQBus is only initialised with a messageDelayInMs
. The number of milliseconds by which the transmission of a new message to the consumer should be delayed is passed here.
A classic initialisation in the context of a dependency injection would look as follows:
using Brainix.GoalsCommon.RabbitMQ.Bus;
...
services.AddSingleton<IBrainixEventBus, TestRabbitMQBus>((messageDelayInMs) => new TestRabbitMQBus(1));
BrainixEvents
All events that should be processed by the RabbitMQ bus must inherit from the generic, abstract class Brainix.GoalsCommon.RabbitMQ.Models.BrainixEvent
. Each event shall contain a clearly defined data structure. The purpose of each event shall be explained in a short DocString.
Each BrainixEvent also has the possibility to declare different event types. This can be done by declaring a public, internal enum with the name EventType
for the respective BrainixEvent (Here it is very important that the name of the enum is written exactly like this).
Here is a short example:
using System;
namespace Brainix.GoalsCommon.RabbitMQ.Models.DomainEvents
{
/// <summary>
/// This event is used for publishing Notifications that were produced by a specific user.
/// </summary>
public class NotificationEvent: BrainixEvent
{
public Guid SenderUserId { get; set; }
public string NotificationContent { get; set; }
public enum EventType
{
Information,
Warning
}
}
}
Routing logic for BrainixEvents
The routing logic in RabbitMQ EventBus is based on topic exchanges and work queues. The underlying idea is that each event is published via its own exchange. Each individual microservice that wants to consume this event has its own queue that is bound to the respective exchange via one or more binding keys. The individual instances of the microservice consume directly from the queue. In concrete terms, this means that when a new Brainix event is published, an exchange with the name of the event is created. If a microservice specifies that it consumes an event, a queue is created and bound to the exchange. Here, all events - regardless of type - are consumed initially. If the consumer specifies that only a certain event types should be consumed, the binding key (between the exchange and the queue) will carry the names of the event types. From now on, only events of that types will be placed in the respective queue. This logic is illustrated by the following diagram:
Publish
To send an event, the method Publish<T>(T brainixEvent, Func<BrainixEventCallback, Task> callbackDelegate) where T : BrainixEvent
must be called from the Brainix.GoalsCommon.RabbitMQ.Bus.IBrainixEventBus
. This will take the event to be sent and optionally a callback function which will be called once the event has been successfully sent to the RabbitMQServer and the reception has been confirmed. By default, an event is initialised with the Default
event type, where the EventTypeNumber
has the value 0. If a special EventType should be set, then the EventTypeNumber
property must be filled with the number of the matching element of the respective EventType
enumeration. Here is a short example:
brainixEventBus.Publish(new NotificationEvent
{
EventTypeNumber = (int) NotificationEvent.EventType.Information,
SenderUserId = new Guid("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"),
NotificationContent = "Hello World!"
}, async (brainixEventCallback) =>
{
if(brainixEventCallback.Success){
//Handle successful publish
}
else
{
//Handle unsuccessful publish
}
});
Consume
To consume an event, the method Subscribe<T>(Func<T, Task> handlerFunction, List<int> eventTypeNumbers) where T : BrainixEvent
must be called from the Brainix.GoalsCommon.RabbitMQ.Bus.IBrainixEventBus
. This receives a handler function which is called as soon as a new event has been consumed. It should be noted that if this function cannot be successfully executed (an exception is caught), the event is put back into the queue and will no longer be consumed. It is also important to note that you cannot call the database directly from the handler function, because you are in a different thread and context. More information on this can be found in the corresponding documentation. Optionally, a list with the numbers of the respective event types that are to be consumed can be given to the method. This parameter determines the individual binding between the queues and exchanges that has already been explained. Here is a short example:
brainixEventBus.Subscribe<NotificationEvent>(async (notificationEvent) =>
{
//Do something with notificationEvent
}, new List<int>
{
(int) NotificationEvent.EventType.Information
});
Testing
For testing with the RabbitMQ bus, the IBrainixEventBus
interface must be implemented by the Brainix.GoalsCommon.RabbitMQ.Bus.TestRabbitMQBus
. Events can be sent and consumers defined in the integration test in the same way as in the normal implementation. However, it is important to note that the handler function of the consumer is always called in a different thread and thus executed with a time delay. This is especially important when writing to cross-thread objects within the integration test and comparing them with the expected object using Assert
. It is advisable to pause the main thread for a few milliseconds to ensure that all other threads have successfully completed their work. Alternatively, you can do the ``assert'' directly in the handler function, which is cleaner in many cases, but does not cover the case where an event is never consumed.
KnowledgeTree
The structure of the knowledge tree is essentially divided into 3 parts - the basic models that form the knowledge tree (in Brainix.GoalsCommon.KnowledgeTree.Models
), the outgrowing static Brainix.GoalsCommon.KnowledgeTree.KnowledgeTreeContent
and the unit tests in Brainix.GoalsCommon.Test.KnowledgeTree.Tests
.
Models
In the following, the basic models that make up the learning tree will be explained. The highest level of the learning tree currently consists of a Brainix.GoalsCommon.KnowledgeTree.Models.Curriculum
. This contains an array of 3 Brainix.GoalsCommon.KnowledgeTree.Models.Category
. These contain an array with an unspecified number of Brainix.GoalsCommon.KnowledgeTree.Models.Skill
. The skills are the most important elements of the learning tree. They always have a SkillId
, which is referenced in different parts of the system. The structure of the learning tree looks as follows:
Curriculum<br> ├─Category1<br> ├─SkillA<br> ├─SkillB<br> ├─SkillC<br> ├─Category2<br> ├─SkillD<br> ├─SkillE<br> ├─Category3<br> ├─SkillF<br> ├─SkillG<br>
In addition, there is the Brainix.GoalsCommon.KnowledgeTree.Models.Categorization.SkillInstace
class that inherits from Brainix.GoalsCommon.KnowledgeTree.Models.Skill
. This contains an additional property Weight
which is used to specify a weight for a skill. There is also the Brainix.GoalsCommon.KnowledgeTree.Models.Categorization.UserSkillState
class, which also inherits from Brainix.GoalsCommon.KnowledgeTree.Models.Skill
. This contains a UserId
property and a KnowledgePoints
property and is used for describing the learning state of a specific student for a skill.
To classify tasks within Brainix using the knowledge tree, the Brainix.GoalsCommon.KnowledgeTree.Models.Categorization
class is used. This contains an enum CompetenceLevel
and an array of Brainix.GoalsCommon.KnowledgeTree.Models.Categorization.SkillInstance
. Within this object, the following rules apply:
- the SkillInstance array must not be empty
- the sum of the weights of the skillinstances within the array must equal 1 (tolerance: 0.01)
- no skills may be duplicated.
- the skills must be declared in
Brainix.GoalsCommon.KnowledgeTree.KnowledgeTreeContent
. - the skills must all come from the same curriculum.
An example initialisation of the object can look like this:
var categorization = new Categorization
{
CompetenceLevel = CompetenceLevel.UnknownApplication,
SkillInstances = new SkillInstance[] {
new SkillInstance {
SkillId = new Guid("51352fc7-1bf8-45d0-92cb-81f4df545bec"),
Weight = 0.5,
},
new SkillInstance {
SkillId = new Guid("1aa2d3f7-cec7-4546-a213-291193b8b6e6"),
Weight = 0.5,
},
},
}
KnowledgeTreeContent
The KnowledgeTreeContent consists of a static, initialised array of curricula. Based on the structure of the learning tree explained above, all skills are listed here in categories and the corresponding curricula. This array is the basis of the learning tree and is also used as the validation basis for all categorisations. In addition, the class contains two methods that are useful for interacting with the learning tree.
Note: Don't change the content of the Knowledgetree if you are not authorised!
Unittests
For the learning tree, some unit tests are implemented in Brainix.GoalsCommon.Test.KnowledgeTree.KnowledgeTreeTests
. The ValidateKnowledgeTreeSnapshot()
test is particularly important here. It compares whether the current array with the curricula from the Brainix.GoalsCommon.KnowledgeTree.KnowledgeTreeContent
is identical to a snapshot stored in Brainix.GoalsCommon.Test/KnowledgeTree/data/knowledgetreesnapshot.json
. As soon as you edit the array with the curricula and add, remove or edit a skill, this unittest will fail. If this happens, you will not be able to upload a new version of the package to NuGet. In order for the unittest to work again, the snapshot must be manually updated first.
Refresh Snapshot
Every time the ValidateKnowledgeTreeSnapshot()
test is executed, a current snapshot of the learning tree is created and saved as a file in Brainix.GoalsCommon.Test/bin/Debug/net5.0/KnowledgeTree/data/knowledgetreesnapshot.json
. If one wants to replace the current snapshot with the freshly generated snapshot, the following command must be executed in the console at the level of the project:
cp .\Brainix.GoalsCommon.Test\bin\Debug\net5.0\KnowledgeTree\data\knowledgetreesnapshot.json .\Brainix.GoalsCommon.Test\KnowledgeTree\data\knowledgetreesnapshot.json
The test will then work again.
Note: Each time a new snapshot is created, it must also be inserted in the ConfigTool.
DataLoad
The DataLoad part of the GoalsCommon Package has two main functions. On the one hand, it provides common basic models for frequently used data types and on the other hand, it contains a DataLoader
with which you can insert manually created DataLoads into the system.
Models
The models listed below can be used as DataContract in requests and as parent class for the models that define the database structure in EntityFramework.
BaseExercise
The base element within Brainix is the Brainix.GoalsCommon.Dataload.Models.BaseExercise
. This is uniquely identified by an ExerciseId
(Guid). In addition, an exercise always belongs to a task. This is referenced by the TaskId
(Guid) (more on this in the next subchapter). Furthermore, there are 4 categories of exercises:
- exercises with automatic feedback
- exercises with individual feedback
- exercises without feedback
- note exercises
The type of an exercise is stored in the enumeration ExerciseType
. Furthermore, an exercise can reference a merke exercise. This reference is stored in the property MerkeExerciseId
. The difficulty level of an exercise is stored in the DifficultyLevel
enumeration. In addition, each exercise has a TypeId
- this is equivalent to the former TypeId of the ExerciseDetails. Finally, an exercise contains a property ExerciseDetails
in which the structure of the respective exercise is described, as well as an ExerciseCorrectAnswer
object in which the solutions to the exercise are stored.
The following rules apply within the exercise:
- the
ExerciseId
must not be empty - the
TaskId
must not be empty - each exercise must have a
TypeId
and anExerciseType
. - the
ExerciseDetails
must not be empty - for exercises without feedback and merke exercises the field
ExerciseCorrectAnswer
must be empty - exercises without feedback and mnemonic exercises must not have a
DifficultyLevel
. - exercises with feedback must have a
DifficultyLevel
. - the
MerkeExerciseId
must be empty for Merke Exercises.
BaseTask
A BaseTask is a set of 1...n BaseExercises that are so interconnected that they cannot be used independently of each other. They must always be processed in the same sequence without interruption. The exercises of a task are stored in the Exercises
list. The second important component of a task is the Categorisation
, which has already been explained. Furthermore, BaseTasks have 4 different types which are stored in the TaskType
enumeration:
- performance task with automatic feedback
- performance task with individual feedback
- information task (tasks that only convey content without the user having to enter anything. These contents are always required by at least one performance task).
- memory task (tasks that only teach basic content at the passive reproduction level of competence)
- story task (tasks that only convey content without the user having to provide input. This content is not required by any performance task.)
- reference (these tasks are initialised with a
TaskId
only. They are only a reference to an already initialised task in another location).
As already mentioned, Performance Tasks can refer to an Information Task. This is done via the InformationTaskId
. In addition, the estimated completion time for tasks is defined in the EstimatedTimeInSeconds
property. By default, each task is contained in a lesson. This is referenced via the OriginLessonId
.
The following rules apply within a task:
- the
TaskId
must not be empty. - for tasks of the reference type, everything except the
TaskId
must be empty. - the
EstimatedTimeInSeconds
must not be less than or equal to 0. - the list of BaseExercises must not be empty.
- a categorisation must be specified for performance tasks.
- the
InformationTaskId
must be empty for Merke, Information and Story Tasks. - performance tasks with individual feedback must have a categorisation.
- performance tasks with automatic feedback must only contain exercises of the type "exercise with automatic feedback".
- performance tasks with individual feedback may only contain exercises of the type "exercise with individual feedback".
- only exercises of the type "merke exercise" may be included in merke tasks.
- Information Tasks and Story Tasks may only contain exercises of the type "exercise without feedback".
BaseCollection
The collection is a dynamic set of performance, information and merke tasks that can be processed by a user either sequentially or in a random order. Since performance tasks can reference an information task, it must also be ensured that between two performance tasks that reference the same information task, there is no merke, information or performance task that references no information task or a completely different information task. This rule forms the structure of a group. A group is a
- Set of 1...n performance tasks all referencing the same information task + the respective information task at position 0 of the group.
- performance task that does not reference an information task.
- merke task
It follows that a collection consists of 1...n groups, which contain the actual tasks. However, the group is a virtual structure and is not needed for the initialisation of a collection. Here, only a list of the TaskIds
of all Performance Tasks is specified. It should be noted that the rules explained above are not violated by the pure order of the Ids. Furthermore, each collection has a BackgroundPicturePath
which points to the picture that is to be displayed in the background during the processing of the collection and a CollectionId
(which must never be empty). Whether the groups of a collection are to be processed sequentially or in a random order is defined by the enumeration OrderType
. Furthermore, each collection initially belongs to a lesson, which is referenced by the LessonId
property. The type of a collection is defined by the property CollectionType
.
DataLoader
The DataLoader is a simple class that is responsible for carrying out the "DataLoad" process for the collection logic. It receives the URLs for the instance of an exercise service and the URLs for the instance of a collection service at initialisation. It also contains an asynchronous method LoadDataLoadElementAsync(DataLoadElement dataLoadElement)
through which the actual DataLoad is executed.
DataLoadElement
The Brainix.GoalsCommon.Dataload.Models.DataLoadElement
is an object that must be initialised to perform a DataLoad. This has three important properties:
Collection
- this holds the BaseCollection that is supposed to be uploaded. This must be initialised according to the rules already explained. It does not have to reference all performance tasks that are contained in theTasks
list. However, it should also not reference any tasks that are not in the task list and for which one is not sure whether they have already been uploaded to this collection service instance.Tasks
The list of tasks should contain all tasks that are supposed to be uploaded - independent of their type.Exercises
This list of exercises should contain all exercises that are supposed to be uploaded - also independent of the type.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 is compatible. net5.0-windows was computed. net6.0 was computed. 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. |
-
net5.0
- Brainix.Common.Classes-v2 (>= 1.0.1)
- Newtonsoft.Json (>= 13.0.1)
- Polly (>= 7.2.2)
- RabbitMQ.Client (>= 6.2.2)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated |
---|---|---|
1.0.2-beta-311 | 211 | 12/9/2022 |
1.0.2-beta-307 | 236 | 12/6/2022 |
1.0.2-beta-305 | 159 | 12/5/2022 |
1.0.2-beta-303 | 136 | 12/5/2022 |
1.0.2-beta-300 | 147 | 11/30/2022 |
1.0.2-beta-298 | 148 | 11/30/2022 |
1.0.2-beta-296 | 153 | 11/30/2022 |
1.0.2-beta-292 | 274 | 11/16/2022 |
1.0.2-beta-290 | 138 | 11/16/2022 |
1.0.2-beta-288 | 150 | 11/16/2022 |
1.0.2-beta-286 | 153 | 11/16/2022 |
1.0.2-beta-283 | 403 | 10/19/2022 |
1.0.2-beta-282 | 178 | 10/18/2022 |
1.0.2-beta-280 | 183 | 10/18/2022 |
1.0.2-beta-279 | 160 | 10/18/2022 |
1.0.2-beta-277 | 190 | 10/14/2022 |
1.0.2-beta-270 | 2,027 | 4/19/2022 |
1.0.2-beta-269 | 198 | 4/19/2022 |
1.0.2-beta-267 | 204 | 4/19/2022 |
1.0.2-beta-264 | 287 | 4/18/2022 |
1.0.2-beta-261 | 194 | 4/15/2022 |
1.0.2-beta-258 | 207 | 4/15/2022 |
1.0.2-beta-255 | 206 | 4/12/2022 |
1.0.2-beta-253 | 260 | 4/5/2022 |
1.0.2-beta-247 | 203 | 3/28/2022 |
1.0.2-beta-244 | 297 | 3/16/2022 |
1.0.2-beta-241 | 189 | 3/13/2022 |
1.0.2-beta-234 | 462 | 3/9/2022 |
1.0.2-beta-232 | 390 | 3/8/2022 |
1.0.2-beta-230 | 198 | 3/8/2022 |
1.0.2-beta-228 | 188 | 3/8/2022 |
1.0.2-beta-226 | 203 | 3/8/2022 |
1.0.2-beta-224 | 199 | 3/8/2022 |
1.0.2-beta-221 | 199 | 3/8/2022 |
1.0.2-beta-217 | 215 | 3/8/2022 |
1.0.2-beta-214 | 213 | 3/6/2022 |
1.0.2-beta-212 | 340 | 3/2/2022 |
1.0.2-beta-209 | 208 | 3/2/2022 |
1.0.2-beta-208 | 764 | 3/2/2022 |
1.0.2-beta-203 | 264 | 3/1/2022 |
1.0.2-beta-200 | 588 | 2/28/2022 |
1.0.2-beta-198 | 206 | 2/27/2022 |
1.0.2-beta-195 | 211 | 2/26/2022 |
1.0.2-beta-193 | 200 | 2/26/2022 |
1.0.2-beta-191 | 203 | 2/26/2022 |
1.0.2-beta-184 | 497 | 2/11/2022 |
1.0.2-beta-181 | 231 | 2/9/2022 |
1.0.2-beta-178 | 457 | 2/5/2022 |
1.0.2-beta-175 | 195 | 2/5/2022 |
1.0.2-beta-173 | 187 | 2/5/2022 |
1.0.2-beta-171 | 270 | 2/4/2022 |
1.0.2-beta-169 | 214 | 2/3/2022 |
1.0.2-beta-165 | 331 | 1/31/2022 |
1.0.2-beta-162 | 221 | 1/31/2022 |
1.0.2-beta-159 | 244 | 1/29/2022 |
1.0.2-beta-154 | 511 | 1/25/2022 |
1.0.2-beta-152 | 208 | 1/25/2022 |
1.0.2-beta-147 | 320 | 1/24/2022 |
1.0.2-beta-145 | 233 | 1/24/2022 |
1.0.2-beta-141 | 231 | 1/21/2022 |
1.0.2-beta-139 | 219 | 1/20/2022 |
1.0.2-beta-132 | 215 | 1/19/2022 |
1.0.2-beta-125 | 375 | 1/18/2022 |
1.0.2-beta-000 | 218 | 1/19/2022 |
1.0.1 | 539 | 1/18/2022 |
1.0.1-beta-120 | 201 | 1/18/2022 |
1.0.0-beta-99 | 214 | 1/17/2022 |
1.0.0-beta-96 | 203 | 1/16/2022 |
1.0.0-beta-94 | 210 | 1/16/2022 |
1.0.0-beta-92 | 216 | 1/16/2022 |
1.0.0-beta-9 | 216 | 1/6/2022 |
1.0.0-beta-88 | 269 | 1/15/2022 |
1.0.0-beta-85 | 232 | 1/14/2022 |
1.0.0-beta-82 | 207 | 1/14/2022 |
1.0.0-beta-80 | 196 | 1/14/2022 |
1.0.0-beta-78 | 218 | 1/14/2022 |
1.0.0-beta-75 | 229 | 1/11/2022 |
1.0.0-beta-73 | 273 | 1/11/2022 |
1.0.0-beta-71 | 206 | 1/11/2022 |
1.0.0-beta-7 | 261 | 1/6/2022 |
1.0.0-beta-69 | 224 | 1/11/2022 |
1.0.0-beta-67 | 215 | 1/11/2022 |
1.0.0-beta-65 | 203 | 1/11/2022 |
1.0.0-beta-63 | 269 | 1/11/2022 |
1.0.0-beta-61 | 196 | 1/11/2022 |
1.0.0-beta-58 | 201 | 1/11/2022 |
1.0.0-beta-56 | 208 | 1/11/2022 |
1.0.0-beta-53 | 227 | 1/10/2022 |
1.0.0-beta-51 | 220 | 1/10/2022 |
1.0.0-beta-50 | 217 | 1/10/2022 |
1.0.0-beta-48 | 214 | 1/10/2022 |
1.0.0-beta-46 | 220 | 1/10/2022 |
1.0.0-beta-44 | 207 | 1/10/2022 |
1.0.0-beta-42 | 292 | 1/8/2022 |
1.0.0-beta-40 | 220 | 1/8/2022 |
1.0.0-beta-4 | 207 | 1/6/2022 |
1.0.0-beta-38 | 226 | 1/8/2022 |
1.0.0-beta-36 | 211 | 1/8/2022 |
1.0.0-beta-35 | 199 | 1/8/2022 |
1.0.0-beta-32 | 215 | 1/8/2022 |
1.0.0-beta-26 | 211 | 1/8/2022 |
1.0.0-beta-24 | 216 | 1/8/2022 |
1.0.0-beta-22 | 201 | 1/8/2022 |
1.0.0-beta-20 | 218 | 1/8/2022 |
1.0.0-beta-15 | 219 | 1/8/2022 |
1.0.0-beta-13 | 213 | 1/7/2022 |
1.0.0-beta-128 | 204 | 1/19/2022 |
1.0.0-beta-112 | 201 | 1/18/2022 |
1.0.0-beta-109 | 202 | 1/18/2022 |
1.0.0-beta-104 | 217 | 1/17/2022 |