WatsonTcp 6.0.8
dotnet add package WatsonTcp --version 6.0.8
NuGet\Install-Package WatsonTcp -Version 6.0.8
<PackageReference Include="WatsonTcp" Version="6.0.8" />
paket add WatsonTcp --version 6.0.8
#r "nuget: WatsonTcp, 6.0.8"
// Install WatsonTcp as a Cake Addin #addin nuget:?package=WatsonTcp&version=6.0.8 // Install WatsonTcp as a Cake Tool #tool nuget:?package=WatsonTcp&version=6.0.8
WatsonTcp
WatsonTcp is the fastest, easiest, most efficient way to build TCP-based clients and servers in C# with integrated framing, reliable transmission, and fast disconnect detection.
IMPORTANT WatsonTcp provides framing to ensure message-level delivery which also dictates that you must either 1) use WatsonTcp for both the server and the client, or, 2) ensure that your client/server exchange messages with the WatsonTcp node using WatsonTcp's framing. Refer to FRAMING.md
for a reference on WatsonTcp message structure.
- If you want a library that doesn't use framing, but has a similar implementation, use SuperSimpleTcp
- If you want a library that doesn't use framing and provides explicit control over how much data to read, use CavemanTcp
.NET Foundation
This project is part of the .NET Foundation along with other projects like the .NET Runtime.
Contributions
Special thanks to the following people for their support and contributions to this project!
@brudo @MrMikeJJ @mikkleini @pha3z @crushedice @marek-petak @ozrecsec @developervariety @NormenSchwettmann @karstennilsen @motridox @AdamFrisby @Job79 @Dijkstra-ru @playingoDEERUX @DuAell @syntacs @zsolt777 @broms95 @Antwns @MartyIX @Jyck @Memphizzz @nirajgenius @cee-sharp @jeverz @cbarraco @DenisBalan @Markonius @Ahmed310 @markashleybell @thechosensausage @JVemon @eatyouroats @bendablegears @Laiteux @fisherman6v6 @wesoos @YorVeX @tovich37 @sancheolz @lunedis
If you'd like to contribute, please jump right into the source code and create a pull request, or, file an issue with your enhancement request.
New in v6.0.x
- Remove unsupported frameworks
- Async version of
SyncMessageReceived
callback - Moving usings inside namespace
- Remove obsolete methods
- Mark non-async APIs obsolete
- Modified test projects to use async
- Ensured background tasks honored cancellation tokens
- Ability to specify a client's GUID before attempting to connect
- Remove obsolete methods
Test Applications
Test projects for both client and server are included which will help you understand and exercise the class library.
SSL
WatsonTcp supports data exchange with or without SSL. The server and client classes include constructors that allow you to include fields for the PFX certificate file and password. An example certificate can be found in the test projects, which has a password of 'password'.
To Stream or Not To Stream...
WatsonTcp allows you to receive messages using either byte arrays or streams. Set Events.MessageReceived
if you wish to consume a byte array, or, set Events.StreamReceived
if you wish to consume a stream.
It is important to note the following:
- When using
Events.MessageReceived
- The message payload is read from the stream and sent to your application
- The event is fired asynchronously and Watson can continue reading messages while your application processes
- When using
Events.StreamReceived
- If the message payload is smaller than
Settings.MaxProxiedStreamSize
, the data is read into aMemoryStream
and sent to your application asynchronously - If the message payload is larger than
Settings.MaxProxiedStreamSize
, the underlying data stream is sent to your application synchronously, and WatsonTcp will wait until your application responds before continuing to read
- If the message payload is smaller than
- Only one of
Events.MessageReceived
andEvents.StreamReceived
should be set;Events.MessageReceived
will be used if both are set
Including Metadata with a Message
Should you with to include metadata with any message, use the Send
or SendAsync
method that allows you to pass in metadata (Dictionary<string, object>
). Refer to the TestClient
, TestServer
, TestClientStream
, and TestServerStream
projects for a full example. Keys must be of type string
.
Note: if you use a class instance as either the value, you'll need to deserialize on the receiving end from JSON.
object myVal = args.Metadata["myKey"];
MyClass instance = myVal.ToObject<MyClass>();
This is not necessary if you are using simple types (int, string, etc). Simply cast to the simple type.
IMPORTANT
Identifying the demarcation between message header and payload is CPU intensive and requires evaluation of the tail end of an internally-managed buffer. This process of evaluation is performed for each byte read until the end of the header is reached. Thus, is it recommended that the metadata property be used sparingly and with very small amounts of data (less than 1KB). When used with large amounts of data, CPU utilization will increase dramatically and response time will be very slow.
Local vs External Connections
IMPORTANT
- If you specify
127.0.0.1
as the listener IP address in WatsonTcpServer, it will only be able to accept connections from within the local host. - To accept connections from other machines:
- Use a specific interface IP address, or
- Use
null
,*
,+
, or0.0.0.0
for the listener IP address (requires admin privileges to listen on any IP address)
- Make sure you create a permit rule on your firewall to allow inbound connections on that port
- If you use a port number under 1024, admin privileges will be required
Running under Mono
.NET Core should always be the preferred option for multi-platform deployments. However, WatsonTcp works well in Mono environments with the .NET Framework to the extent that we have tested it. It is recommended that when running under Mono, you execute the containing EXE using --server and after using the Mono Ahead-of-Time Compiler (AOT). Note that TLS 1.2 is hard-coded, which may need to be downgraded to TLS in Mono environments.
NOTE: Windows accepts '0.0.0.0' as an IP address representing any interface. On Mac and Linux you must be specified ('127.0.0.1' is also acceptable, but '0.0.0.0' is NOT).
mono --aot=nrgctx-trampolines=8096,nimt-trampolines=8096,ntrampolines=4048 --server myapp.exe
mono --server myapp.exe
Examples
The following examples show a simple client and server example using WatsonTcp without SSL and consuming messages using byte arrays instead of streams. For full examples, please refer to the Test.*
projects.
Server
using WatsonTcp;
static void Main(string[] args)
{
WatsonTcpServer server = new WatsonTcpServer("127.0.0.1", 9000);
server.Events.ClientConnected += ClientConnected;
server.Events.ClientDisconnected += ClientDisconnected;
server.Events.MessageReceived += MessageReceived;
server.Callbacks.SyncRequestReceivedAsync = SyncRequestReceived;
server.Start();
// list clients
IEnumerable<ClientMetadata> clients = server.ListClients();
// send a message
await server.SendAsync([guid], "Hello, client!");
// send a message with metadata
Dictionary<string, object> md = new Dictionary<string, object>();
md.Add("foo", "bar");
await server.SendAsync([guid], "Hello, client! Here's some metadata!", md);
// send and wait for a response
try
{
SyncResponse resp = await server.SendAndWaitAsync(
[guid],
5000,
"Hey, say hello back within 5 seconds!");
Console.WriteLine("My friend says: " + Encoding.UTF8.GetString(resp.Data));
}
catch (TimeoutException)
{
Console.WriteLine("Too slow...");
}
}
static void ClientConnected(object sender, ConnectionEventArgs args)
{
Console.WriteLine("Client connected: " + args.Client.ToString());
}
static void ClientDisconnected(object sender, DisconnectionEventArgs args)
{
Console.WriteLine(
"Client disconnected: "
+ args.Client.ToString()
+ ": "
+ args.Reason.ToString());
}
static void MessageReceived(object sender, MessageReceivedEventArgs args)
{
Console.WriteLine(
"Message from "
+ args.Client.ToString()
+ ": "
+ Encoding.UTF8.GetString(args.Data));
}
static async Task<SyncResponse> SyncRequestReceived(SyncRequest req)
{
return new SyncResponse(req, "Hello back at you!");
}
Client
using WatsonTcp;
static void Main(string[] args)
{
WatsonTcpClient client = new WatsonTcpClient("127.0.0.1", 9000);
client.Events.ServerConnected += ServerConnected;
client.Events.ServerDisconnected += ServerDisconnected;
client.Events.MessageReceived += MessageReceived;
client.Callbacks.SyncRequestReceivedAsync = SyncRequestReceived;
client.Connect();
// check connectivity
Console.WriteLine("Am I connected? " + client.Connected);
// send a message
client.Send("Hello!");
// send a message with metadata
Dictionary<string, object> md = new Dictionary<string, object>();
md.Add("foo", "bar");
await client.SendAsync("Hello, client! Here's some metadata!", md);
// send and wait for a response
try
{
SyncResponse resp = await client.SendAndWaitAsync(
5000,
"Hey, say hello back within 5 seconds!");
Console.WriteLine("My friend says: " + Encoding.UTF8.GetString(resp.Data));
}
catch (TimeoutException)
{
Console.WriteLine("Too slow...");
}
}
static void MessageReceived(object sender, MessageReceivedEventArgs args)
{
Console.WriteLine("Message from server: " + Encoding.UTF8.GetString(args.Data));
}
static void ServerConnected(object sender, ConnectionEventArgs args)
{
Console.WriteLine("Server connected");
}
static void ServerDisconnected(object sender, DisconnectionEventArgs args)
{
Console.WriteLine("Server disconnected");
}
static async Task<SyncResponse> SyncRequestReceived(SyncRequest req)
{
return new SyncResponse(req, "Hello back at you!");
}
Example with SSL
The examples above can be modified to use SSL as follows. No other changes are needed. Ensure that the certificate is exported as a PFX file and is resident in the directory of execution.
// server
WatsonTcpServer server = new WatsonTcpServer("127.0.0.1", 9000, "test.pfx", "password");
server.Settings.AcceptInvalidCertificates = true;
server.Settings.MutuallyAuthenticate = true;
server.Start();
// client
WatsonTcpClient client = new WatsonTcpClient("127.0.0.1", 9000, "test.pfx", "password");
client.Settings.AcceptInvalidCertificates = true;
client.Settings.MutuallyAuthenticate = true;
client.Connect();
Example with Streams
Refer to the Test.ClientStream
and Test.ServerStream
projects for a full example.
// server
WatsonTcpServer server = new WatsonTcpServer("127.0.0.1", 9000);
server.Events.ClientConnected += ClientConnected;
server.Events.ClientDisconnected += ClientDisconnected;
server.Events.StreamReceived += StreamReceived;
server.Start();
static void StreamReceived(object sender, StreamReceivedEventArgs args)
{
long bytesRemaining = args.ContentLength;
int bytesRead = 0;
byte[] buffer = new byte[65536];
using (MemoryStream ms = new MemoryStream())
{
while (bytesRemaining > 0)
{
bytesRead = args.DataStream.Read(buffer, 0, buffer.Length);
if (bytesRead > 0)
{
ms.Write(buffer, 0, bytesRead);
bytesRemaining -= bytesRead;
}
}
}
Console.WriteLine(
"Stream received from "
+ args.Client.ToString()
+ ": "
+ Encoding.UTF8.GetString(ms.ToArray()));
}
// client
WatsonTcpClient client = new WatsonTcpClient("127.0.0.1", 9000);
client.Events.ServerConnected += ServerConnected;
client.Events.ServerDisconnected += ServerDisconnected;
client.Events.StreamReceived += StreamReceived;
client.Connect();
static void StreamReceived(object sender, StreamReceivedEventArgs args)
{
long bytesRemaining = args.ContentLength;
int bytesRead = 0;
byte[] buffer = new byte[65536];
using (MemoryStream ms = new MemoryStream())
{
while (bytesRemaining > 0)
{
bytesRead = args.DataStream.Read(buffer, 0, buffer.Length);
if (bytesRead > 0)
{
ms.Write(buffer, 0, bytesRead);
bytesRemaining -= bytesRead;
}
}
}
Console.WriteLine("Stream received from server: " + Encoding.UTF8.GetString(ms.ToArray()));
}
Specifying a Client GUID
If you wish to specify a client's GUID, you can modify WatsonTcpClient.Settings.Guid
prior to calling WatsonTcpClient.Connect()
.
WatsonTcpClient client = new WatsonTcpClient("127.0.0.1", 9000);
client.Events.ServerConnected += ServerConnected;
client.Events.ServerDisconnected += ServerDisconnected;
client.Events.StreamReceived += StreamReceived;
client.Settings.Guid = Guid.Parse("12345678-1234-1234-123456781234");
client.Connect();
Troubleshooting
The first step in troubleshooting is to implement a logging method and attach it to Settings.Logger
, and as a general best practice while debugging, set Settings.DebugMessages
to true
.
client.Settings.DebugMessages = true;
client.Settings.Logger = MyLoggerMethod;
private void MyLoggerMethod(Severity sev, string msg)
{
Console.WriteLine(sev.ToString() + ": " + msg);
}
Additionally it is recommended that you implement the Events.ExceptionEncountered
event.
client.Events.ExceptionEncountered += MyExceptionEvent;
private void MyExceptionEvent(object sender, ExceptionEventArgs args)
{
Console.WriteLine(args.Json);
}
Disconnection Handling
The project TcpTest (https://github.com/jchristn/TcpTest) was built specifically to provide a reference for WatsonTcp to handle a variety of disconnection scenarios. The disconnection tests for which WatsonTcp is evaluated include:
Test case | Description | Pass/Fail |
---|---|---|
Server-side dispose | Graceful termination of all client connections | PASS |
Server-side client removal | Graceful termination of a single client | PASS |
Server-side termination | Abrupt termination due to process abort or CTRL-C | PASS |
Client-side dispose | Graceful termination of a client connection | PASS |
Client-side termination | Abrupt termination due to a process abort or CTRL-C | PASS |
Network interface down | Network interface disabled or cable removed | Partial (see below) |
Additionally, as of v4.3.0, support for TCP keepalives has been added to WatsonTcp, primarily to address the issue of a network interface being shut down, the cable unplugged, or the media otherwise becoming unavailable. It is important to note that keepalives are supported in .NET Core and .NET Framework, but NOT .NET Standard. As of this release, .NET Standard provides no facilities for TCP keepalives.
TCP keepalives are NOT enabled by default. To enable and configure:
server.Keepalive.EnableTcpKeepAlives = true;
server.Keepalive.TcpKeepAliveInterval = 5; // seconds to wait before sending subsequent keepalive
server.Keepalive.TcpKeepAliveTime = 5; // seconds to wait before sending a keepalive
server.Keepalive.TcpKeepAliveRetryCount = 5; // number of failed keepalive probes before terminating connection
Some important notes about TCP keepalives:
- Keepalives only work in .NET Core and .NET Framework
Keepalive.TcpKeepAliveRetryCount
is only applicable to .NET Core; for .NET Framework, this value is forced to 10
Disconnecting Idle Clients
If you wish to have WatsonTcpServer automatically disconnect clients that have been idle for a period of time, set WatsonTcpServer.IdleClientTimeoutSeconds
to a positive integer. Receiving a message from a client automatically resets their timeout. Client timeouts are evaluated every 5 seconds by Watson, so the disconnection may not be precise (for instance, if you use 7 seconds as your disconnect interval).
Donations
If you would like to financially support my efforts, first of all, thank you! Please refer to DONATIONS.md.
Version History
Please refer to CHANGELOG.md for details.
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 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. |
.NET Framework | net462 is compatible. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 is compatible. net481 was computed. |
-
.NETFramework 4.6.2
- System.Text.Json (>= 8.0.5)
-
.NETFramework 4.8
- System.Text.Json (>= 8.0.5)
-
net6.0
- System.Text.Json (>= 8.0.5)
-
net8.0
- System.Text.Json (>= 8.0.5)
NuGet packages (12)
Showing the top 5 NuGet packages that depend on WatsonTcp:
Package | Downloads |
---|---|
BigQ.Client
BigQ is a messaging platform using TCP sockets and websockets featuring sync, async, channel, and private communications. This package includes the BigQ client and associated libraries. |
|
BigQ.Server
BigQ is a messaging platform using TCP sockets and websockets featuring sync, async, channel, and private communications. This package includes the BigQ server and associated libraries. |
|
CoreRemoting
Easy to use Remoting library for .NET Core and .NET Framework |
|
WatsonMesh
A simple C# mesh networking library using TCP (with or without SSL) with integrated framing for reliable transmission and receipt of data amongst multiple nodes. Does not support NAT. |
|
WatsonCluster
A simple C# class using Watson TCP to enable a one-to-one high availability cluster. Events are used to notify the encompassing application when the cluster is healthy (client and server connected), unhealthy (client or server disconnected), or a message is received. |
GitHub repositories (1)
Showing the top 1 popular GitHub repositories that depend on WatsonTcp:
Repository | Stars |
---|---|
h4lfheart/FortnitePorting
Automation of the Fortnite Porting Process
|
Version | Downloads | Last updated |
---|---|---|
6.0.8 | 62 | 12/23/2024 |
6.0.6 | 6,554 | 5/1/2024 |
6.0.5 | 3,912 | 1/16/2024 |
6.0.4 | 612 | 1/10/2024 |
6.0.3 | 400 | 1/10/2024 |
6.0.2 | 397 | 1/9/2024 |
6.0.1 | 4,422 | 1/3/2024 |
6.0.0 | 704 | 12/18/2023 |
5.1.7 | 8,474 | 8/25/2023 |
5.1.6 | 1,094 | 8/13/2023 |
5.1.5 | 4,495 | 7/4/2023 |
5.1.3 | 1,766 | 6/14/2023 |
5.1.2 | 2,697 | 5/4/2023 |
5.1.1 | 2,632 | 3/16/2023 |
5.1.0 | 6,933 | 12/27/2022 |
5.0.11 | 4,431 | 12/1/2022 |
5.0.10 | 899 | 12/1/2022 |
5.0.9 | 901 | 11/30/2022 |
5.0.6 | 1,020 | 11/28/2022 |
5.0.5 | 1,119 | 11/26/2022 |
5.0.4 | 905 | 11/26/2022 |
5.0.3 | 943 | 11/23/2022 |
5.0.2 | 913 | 11/23/2022 |
5.0.1 | 911 | 11/22/2022 |
4.8.14.14 | 8,837 | 8/16/2022 |
4.8.14.13 | 996 | 8/15/2022 |
4.8.14.12 | 962 | 8/15/2022 |
4.8.14.11 | 2,904 | 5/30/2022 |
4.8.14.10 | 4,319 | 3/20/2022 |
4.8.14.9 | 1,334 | 3/9/2022 |
4.8.14.8 | 16,496 | 1/13/2022 |
4.8.14.7 | 1,123 | 1/7/2022 |
4.8.14.6 | 6,888 | 12/10/2021 |
4.8.14.5 | 869 | 12/10/2021 |
4.8.14.4 | 4,153 | 11/20/2021 |
4.8.14.3 | 1,258 | 11/15/2021 |
4.8.14.2 | 3,738 | 11/12/2021 |
4.8.14.1 | 4,081 | 10/13/2021 |
4.8.14 | 2,934 | 9/1/2021 |
4.8.13 | 954 | 9/1/2021 |
4.8.12 | 5,006 | 8/23/2021 |
4.8.11 | 1,330 | 8/17/2021 |
4.8.10 | 2,281 | 7/13/2021 |
4.8.9 | 1,019 | 7/13/2021 |
4.8.8 | 1,110 | 7/7/2021 |
4.8.7 | 972 | 7/6/2021 |
4.8.6 | 2,931 | 6/1/2021 |
4.8.5 | 3,142 | 4/13/2021 |
4.8.4 | 1,239 | 4/1/2021 |
4.8.3 | 994 | 3/31/2021 |
4.8.2 | 1,310 | 3/20/2021 |
4.8.1 | 1,283 | 3/15/2021 |
4.8.0 | 5,070 | 1/20/2021 |
4.7.1.2 | 1,308 | 1/19/2021 |
4.7.1.1 | 2,053 | 1/2/2021 |
4.7.1 | 1,611 | 12/25/2020 |
4.7.0.1 | 1,197 | 12/18/2020 |
4.7.0 | 1,157 | 12/9/2020 |
4.6.0.2 | 1,152 | 12/7/2020 |
4.6.0.1 | 1,282 | 11/22/2020 |
4.6.0 | 1,236 | 11/17/2020 |
4.5.0.3 | 1,207 | 11/15/2020 |
4.5.0.2 | 1,193 | 11/13/2020 |
4.5.0.1 | 1,089 | 11/11/2020 |
4.4.0.6 | 1,633 | 10/25/2020 |
4.4.0.5 | 1,103 | 10/25/2020 |
4.4.0.4 | 1,136 | 10/22/2020 |
4.4.0.3 | 1,182 | 10/20/2020 |
4.4.0.2 | 1,120 | 10/20/2020 |
4.4.0.1 | 1,330 | 10/10/2020 |
4.4.0 | 1,156 | 10/6/2020 |
4.3.0.3 | 1,628 | 9/20/2020 |
4.3.0.2 | 1,188 | 9/14/2020 |
4.3.0.1 | 1,435 | 9/8/2020 |
4.3.0 | 2,410 | 9/1/2020 |
4.2.0 | 4,224 | 8/11/2020 |
4.1.12.7 | 3,112 | 7/31/2020 |
4.1.12.6 | 1,612 | 7/29/2020 |
4.1.12.5 | 1,199 | 7/29/2020 |
4.1.12.4 | 2,617 | 6/27/2020 |
4.1.12.3 | 1,244 | 6/27/2020 |
4.1.12.2 | 1,195 | 6/26/2020 |
4.1.12.1 | 1,192 | 6/25/2020 |
4.1.12 | 1,135 | 6/25/2020 |
4.1.11 | 6,990 | 5/15/2020 |
4.1.10 | 4,035 | 5/1/2020 |
4.1.9 | 1,154 | 5/1/2020 |
4.1.8 | 4,477 | 4/30/2020 |
4.1.7 | 1,366 | 4/30/2020 |
4.1.6 | 1,402 | 4/29/2020 |
4.1.5 | 3,643 | 4/23/2020 |
4.1.4 | 1,425 | 4/19/2020 |
4.1.3 | 1,414 | 4/19/2020 |
4.1.2 | 1,780 | 4/17/2020 |
4.1.1 | 1,341 | 4/17/2020 |
4.1.0 | 1,993 | 4/15/2020 |
4.0.2 | 2,090 | 4/10/2020 |
4.0.1 | 1,185 | 4/10/2020 |
4.0.0 | 1,785 | 4/9/2020 |
3.1.4 | 5,501 | 3/26/2020 |
3.1.3 | 2,262 | 3/23/2020 |
3.1.2 | 1,465 | 3/23/2020 |
3.1.1 | 1,804 | 3/23/2020 |
3.1.0 | 2,153 | 3/21/2020 |
3.0.3 | 15,334 | 2/28/2020 |
3.0.2 | 4,066 | 2/25/2020 |
3.0.1 | 4,013 | 2/22/2020 |
3.0.0 | 1,181 | 2/22/2020 |
2.2.2 | 6,789 | 2/15/2020 |
2.2.1 | 2,723 | 2/13/2020 |
2.2.0 | 22,372 | 12/21/2019 |
2.1.7 | 20,174 | 11/7/2019 |
2.1.6 | 1,202 | 10/22/2019 |
2.1.5 | 1,210 | 10/20/2019 |
2.1.4 | 1,206 | 10/10/2019 |
2.1.3 | 1,273 | 10/5/2019 |
2.1.2 | 1,253 | 10/1/2019 |
2.1.1 | 1,239 | 10/1/2019 |
2.1.0 | 1,225 | 10/1/2019 |
2.0.8 | 1,204 | 9/25/2019 |
2.0.7 | 1,230 | 9/22/2019 |
2.0.6 | 1,642 | 9/19/2019 |
2.0.5 | 2,349 | 9/10/2019 |
2.0.4 | 1,578 | 9/8/2019 |
2.0.3 | 1,551 | 9/8/2019 |
2.0.2 | 1,625 | 9/7/2019 |
1.3.12 | 2,523 | 6/25/2019 |
1.3.7 | 1,316 | 6/13/2019 |
1.3.6 | 3,369 | 5/12/2019 |
1.3.5 | 4,656 | 5/8/2019 |
1.3.3 | 1,473 | 5/7/2019 |
1.3.0 | 1,541 | 5/6/2019 |
1.2.0 | 1,538 | 3/30/2019 |
1.1.7 | 2,115 | 3/10/2019 |
1.1.6 | 4,398 | 2/25/2018 |
1.1.5 | 1,642 | 2/24/2018 |
1.1.4 | 1,708 | 1/11/2018 |
1.1.3 | 1,676 | 9/14/2017 |
1.1.2 | 3,610 | 3/29/2017 |
1.1.1 | 1,620 | 3/11/2017 |
1.1.0 | 1,826 | 3/8/2017 |
1.0.10 | 1,948 | 3/6/2017 |
1.0.9 | 1,591 | 3/6/2017 |
1.0.7 | 1,787 | 3/2/2017 |
1.0.6 | 1,702 | 2/26/2017 |
Breaking changes, move towards async