-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathFirehose.cs
More file actions
129 lines (109 loc) · 4.67 KB
/
Copy pathFirehose.cs
File metadata and controls
129 lines (109 loc) · 4.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
using System.Net.Http;
using System.Net.WebSockets;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
using dnproto.repo;
namespace dnproto.firehose;
public class Firehose
{
/// <summary>
/// Listens to a Bluesky firehose and sends messages back to caller.
///
/// https://atproto.com/specs/event-stream#streaming-wire-protocol-v0
///
/// "Every WebSocket frame contains two DAG-CBOR objects,
/// with bytes concatenated together: a header (indicating message type),
/// and the actual message."
///
/// In the second object (message), there is a property called "blocks"
/// that contains a byte array of records, in repo format.
/// You can walk this byte array like a repo.
///
/// See the repo directory (Repo.cs) for how to walk a repo.
///
/// </summary>
/// <param name="arguments"></param>
/// <exception cref="ArgumentException"></exception>
public static void Listen(string url, Func<DagCborObject, DagCborObject, bool> messageCallback)
{
if (string.IsNullOrEmpty(url)) return;
Firehose.ListenAsync(url, messageCallback).Wait();
}
public static async Task ListenAsync(string url, Func<DagCborObject, DagCborObject, bool> messageCallback)
{
if (string.IsNullOrEmpty(url)) return;
using (ClientWebSocket ws = new ClientWebSocket())
{
ws.Options.SetRequestHeader("User-Agent", "dnproto/1.0");
ws.Options.SetRequestHeader("Accept", "application/json");
ws.Options.SetRequestHeader("Content-Type", "application/json");
Uri uri = new Uri(url);
await ws.ConnectAsync(uri, CancellationToken.None);
//
// Listen for messages
//
bool keepGoing = true;
try
{
while (ws.State == WebSocketState.Open && keepGoing)
{
ArraySegment<byte> buffer = new ArraySegment<byte>(new byte[1024]);
WebSocketReceiveResult? result = null;
using (var ms = new MemoryStream())
{
//
// Read until the end of the message.
// It might arrive in multiple chunks.
//
do
{
result = await ws.ReceiveAsync(buffer, CancellationToken.None);
//
// Check if server is closing the connection
//
if (result.MessageType == WebSocketMessageType.Close)
{
// Complete the close handshake
if (ws.State == WebSocketState.CloseReceived)
{
await ws.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Server closed", CancellationToken.None);
}
return;
}
if (result.Count > 0 && buffer.Array != null)
{
ms.Write(buffer.Array, buffer.Offset, result.Count);
}
} while (!result.EndOfMessage && ws.State == WebSocketState.Open);
//
// Reset memory stream
//
ms.Seek(0, SeekOrigin.Begin);
//
// The first DAG-CBOR object: the header
//
DagCborObject? header = DagCborObject.ReadFromStream(ms);
//
// The second DAG-CBOR object: the message
//
DagCborObject? body = DagCborObject.ReadFromStream(ms);
//
// Send back to caller
//
keepGoing = messageCallback(header, body);
}
}
}
catch (WebSocketException ex) when (ex.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely)
{
// Server closed connection - this is expected during shutdown
}
// Close gracefully if still open
if (ws.State == WebSocketState.Open)
{
await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "Client closing", CancellationToken.None);
}
}
}
}