{"id":18603043,"url":"https://github.com/devlooped/websocketeer","last_synced_at":"2025-04-10T19:31:27.473Z","repository":{"id":37955546,"uuid":"409392711","full_name":"devlooped/WebSocketeer","owner":"devlooped","description":"High-performance intuitive API for Azure Web PubSub protobuf subprotocol","archived":false,"fork":false,"pushed_at":"2024-04-13T00:14:36.000Z","size":118,"stargazers_count":11,"open_issues_count":6,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2024-05-01T09:37:17.341Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/devlooped.png","metadata":{"files":{"readme":"readme.md","changelog":"changelog.md","contributing":null,"funding":null,"license":"license.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"devlooped"}},"created_at":"2021-09-23T00:10:20.000Z","updated_at":"2024-06-13T14:42:31.131Z","dependencies_parsed_at":"2024-06-13T14:42:28.731Z","dependency_job_id":"9b6c96fd-656a-46fa-95fe-a88ade77db8f","html_url":"https://github.com/devlooped/WebSocketeer","commit_stats":{"total_commits":65,"total_committers":5,"mean_commits":13.0,"dds":0.3076923076923077,"last_synced_commit":"efe0c4cdf1896115085f9dfbb9255419ce170363"},"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devlooped%2FWebSocketeer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devlooped%2FWebSocketeer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devlooped%2FWebSocketeer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devlooped%2FWebSocketeer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/devlooped","download_url":"https://codeload.github.com/devlooped/WebSocketeer/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248281400,"owners_count":21077423,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-07T02:13:21.716Z","updated_at":"2025-04-10T19:31:22.455Z","avatar_url":"https://github.com/devlooped.png","language":"C#","funding_links":["https://github.com/sponsors/devlooped","https://github.com/sponsors"],"categories":[],"sub_categories":[],"readme":"![Icon](https://raw.githubusercontent.com/devlooped/WebSocketeer/main/assets/img/icon.png) WebSocketeer\n============\n\nA thin, intuitive, idiomatic and high-performance API for \nAzure Web PubSub [protobuf subprotocol](https://docs.microsoft.com/en-us/azure/azure-web-pubsub/reference-protobuf-webpubsub-subprotocol).\n\n[![Version](https://img.shields.io/nuget/v/WebSocketeer.svg?color=royalblue)](https://www.nuget.org/packages/WebSocketeer)\n[![Downloads](https://img.shields.io/nuget/dt/WebSocketeer.svg?color=green)](https://www.nuget.org/packages/WebSocketeer)\n[![License](https://img.shields.io/github/license/devlooped/WebSocketeer.svg?color=blue)](https://github.com/devlooped/WebSocketeer/blob/main/license.txt)\n[![Build](https://github.com/devlooped/WebSocketeer/workflows/build/badge.svg?branch=main)](https://github.com/devlooped/WebSocketeer/actions)\n\n# What\n\nAzure Web PubSub [protobuf subprotocol](https://docs.microsoft.com/en-us/azure/azure-web-pubsub/reference-protobuf-webpubsub-subprotocol) \nis super awesome and general purpose and I can see endless applications \nfor this new service from Azure. The message-based nature of its \"API\" is \nnot very intuitive or idiomatic for a dotnet developer though, I think. \n\nI wanted to create a super thin layer on top that didn't incur unnecessary \nallocations or buffer handling or extra threads, since that would detract \nfrom the amazing work on performance that .NET 6 brings to the table. \nI use the [best practices](https://docs.microsoft.com/en-us/aspnet/core/grpc/performance?view=aspnetcore-5.0#send-binary-payloads) \nfor sending binary payloads using low-level (and quite new!) protobuf \nAPIs for avoiding unnecessary buffer creation/handling.\n\nIn order to also squeeze every bit of performance, this project uses the \nprotobuf subprotocol exclusively, even though there is support in the service \nfor [JSON](https://docs.microsoft.com/en-us/azure/azure-web-pubsub/reference-json-webpubsub-subprotocol) \npayloads.\n\nThe actual binary payloads you send/receive can of course be decoded into \nany format you need, including JSON if you just encode/decode it as UTF8 bytes.\n\n\u003c!-- #content --\u003e\n# Usage\n\nFirst acquire a proper client access URI for Azure Web PubSub using the \nofficial client API, such as:\n\n```csharp\nvar serviceClient = new WebPubSubServiceClient([WEB_PUB_SUB_CONNECTION_STRING], [HUB_NAME]);\nvar serviceUri = serviceClient.GenerateClientAccessUri(\n    userId: Guid.NewGuid().ToString(\"N\"),\n    roles:  new[]\n    {\n        \"webpubsub.joinLeaveGroup\",\n        \"webpubsub.sendToGroup\"\n    });\n```\n\nNext simply connect the `WebSocketeer`:\n\n```csharp\nawait using IWebSocketeer socketeer = WebSocketeer.ConnectAsync(serviceUri);\n```\n\n\u003e NOTE: the `IWebSocketeer` interface implements both `IAsyncDisposable`, \n\u003e which allows the `await using` pattern above, but also the regular \n\u003e `IDisposable` interface. The former will perform a graceful `WebSocket` \n\u003e disconnect/close. The latter will simply dispose the underlying `WebSocket`.\n\n\nAt this point, the `socketeer` variable contains a properly connected \nWeb PubSub client, and you can inspect its `ConnectionId` and `UserId`\nproperties, for example. \n\nNext step is perhaps to join some groups:\n\n```csharp\nIWebSocketeerGroup group = await socketeer.JoinAsync(\"MyGroup\");\n```\n\nThe `IWebSocketeerGroup` is an observable of `ReadOnlyMemory\u003cbyte\u003e`, exposing \nthe incoming messages to that group, and it also provides a \n`SendAsync(ReadOnlyMemory\u003cbyte\u003e message)` method to post messages to the group.\n\nTo write all incoming messages for the group to the console, you could \nwrite:\n\n```csharp\nusing var subscription = group.Subscribe(bytes =\u003e \n    Console.WriteLine(Encoding.UTF8.GetString(bytes.Span)));\n```\n\nIn order to start processing incoming messages, though, you need to start \nthe socketeer \"message loop\" first. This would typically be done on a separate thread, \nusing `Task.Run`, for example:\n\n```csharp\nvar started = Task.Run(() =\u003e socketeer.RunAsync());\n```\n\nThe returned task from `RunAsync` will remain in progress until the socketeer is disposed, \nor the underlying `WebSocket` is closed (either by the client or the server), or when an \noptional cancellation token passed to it is cancelled.\n\nYou can also send messages to a group you haven't joined (provided the roles \nspecified when opening the connection allow it) via the `IWebSocketeer.SendAsync` \nmethod too:\n\n```csharp\nawait socketeer.SendAsync(\"YourGroup\", Encoding.UTF8.GetBytes(\"Hello World\"));\n```\n\n## Advanced Scenarios\n\n### Accessing Joined Group\n\nSometimes, it's useful to perform group join up-front, but at some \nlater time you might also need to get the previously joined group \nfrom the same `IWebSocketeer` instance. \n\n```csharp\nIWebSocketeer socketeer = /* connect, join some groups, etc. */;\n\n// If group hasn't been joined previously, no incoming messages would arrive in this case.\nIWebSocketeerGroup group = socketeer.Joined(\"incoming\");\ngroup.Subscribe(x =\u003e /* process incoming */);\n```\n\n\n### Handling the WebSocket\n\nYou can alternatively handle the `WebSocket` yourself. Instead of passing the \nservice `Uri` to `ConnectAsync`, you can create and connect a `WebSocket` manually \nand pass it to the `WebSocketeer.ConnectAsync(WebSocket, CancellationToken)` overload.\n\nIn this case, it's important to remember to add the `protobuf.webpubsub.azure.v1` \nrequired subprotocol:\n\n```csharp\nusing Devlooped.Net;\n\nvar client = new ClientWebSocket();\nclient.Options.AddSubProtocol(\"protobuf.webpubsub.azure.v1\");\n\nawait client.ConnectAsync(serverUri, CancellationToken.None);\n\nawait using var socketeer = WebSocketeer.ConnectAsync(client);\n```\n\n\n### Split Request/Response Groups\n\nYou may want to simulate request/response communication patterns over the \nsocketeer. In cases like this, you would typically do the following:\n\n- Server joined to a client-specific group, such as `SERVER_ID-CLIENT_ID` \n  (with a `[TO]-[FROM]` format, so, TO=server, FROM=client)\n- Server replying to requests in that group by sending responses to \n  `CLIENT_ID-SERVER_ID` (TO=client, FROM=server);\n- Client joined to the responses group `CLIENT_ID-SERVER_ID` and sending \n  requests as needed to `SERVER_ID-CLIENT_ID`.\n\nNote that the client *must not* join the `SERVER_ID-CLIENT_ID` group because \notherwise it would *also* receive its own messages that are intended for the \nserver only. Likewise, the server cannot join the `CLIENT_ID-SERVER_ID` group \neither. This is why this pattern might be more common than it would otherwise\nseem.\n\nServer-side:\n\n```csharp\nIWebSocketeer socketeer = ...;\nvar serverId = socketeer.UserId;\n\n// Perhaps from an initial exchange over a shared channel\nvar clientId = ...;\n\nawait using IWebSocketeerGroup clientChannel = socketeer.Split(\n    await socketeer.JoinAsync($\"{serverId}-{clientId}\"), \n    $\"{clientId}-{serverId}\");\n\nclientChannel.Subscribe(async x =\u003e \n{\n    // do some processing on incoming requests.\n    ...\n    // send a response via the outgoing group\n    await clientChannel.SendAsync(response);\n});\n```\n\nClient-side:\n\n```csharp\nIWebSocketeer socketeer = ...;\nvar clientId = socketeer.UserId;\n\n// Perhaps a known identifier, or looked up somehow\nvar serverId = ...;\n\nawait using IWebSocketeerGroup serverChannel = socketeer.Split(\n    await socketeer.JoinAsync($\"{clientId}-{serverId}\"\"),\n    $\"{serverId}-{clientId}\");\n\nserverChannel.Subscribe(async x =\u003e /* process responses */);\nawait serverChannel.SendAsync(request);\n```\n\n\u003c!-- include https://github.com/devlooped/sponsors/raw/main/footer.md --\u003e\n# Sponsors \n\n\u003c!-- sponsors.md --\u003e\n[![Clarius Org](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/clarius.png \"Clarius Org\")](https://github.com/clarius)\n[![Christian Findlay](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/MelbourneDeveloper.png \"Christian Findlay\")](https://github.com/MelbourneDeveloper)\n[![C. Augusto Proiete](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/augustoproiete.png \"C. Augusto Proiete\")](https://github.com/augustoproiete)\n[![Kirill Osenkov](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/KirillOsenkov.png \"Kirill Osenkov\")](https://github.com/KirillOsenkov)\n[![MFB Technologies, Inc.](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/MFB-Technologies-Inc.png \"MFB Technologies, Inc.\")](https://github.com/MFB-Technologies-Inc)\n[![SandRock](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/sandrock.png \"SandRock\")](https://github.com/sandrock)\n[![Eric C](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/eeseewy.png \"Eric C\")](https://github.com/eeseewy)\n[![Andy Gocke](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/agocke.png \"Andy Gocke\")](https://github.com/agocke)\n\n\n\u003c!-- sponsors.md --\u003e\n\n[![Sponsor this project](https://raw.githubusercontent.com/devlooped/sponsors/main/sponsor.png \"Sponsor this project\")](https://github.com/sponsors/devlooped)\n\u0026nbsp;\n\n[Learn more about GitHub Sponsors](https://github.com/sponsors)\n\n\u003c!-- https://github.com/devlooped/sponsors/raw/main/footer.md --\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevlooped%2Fwebsocketeer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdevlooped%2Fwebsocketeer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevlooped%2Fwebsocketeer/lists"}