diff --git a/README.md b/README.md index 5a33158..f710235 100644 --- a/README.md +++ b/README.md @@ -5,22 +5,33 @@ Notifying is backed up by [SignalR](https://dotnet.microsoft.com/en-us/apps/aspn > Postmen have a legendary aura. A ring at the doorbell may inflame a sense of expectation, suspense, secrecy, hazard or even intrigue. Ringing twice may imply a warning that trouble is on the way or an appeal to make the coast clear. ~ "The postman always rings twice", Erik Pevernagie -## LeanPipe parts +## Core concepts -In order to receive notifications via LeanPipe one would need a publisher server and at least one of the provided clients. +Backend defines **Topics** including parameters they have and **Notifications** that may be received by the clients that **Subscribe** to them. +LeanPipe Backend, which also handles **Publishing** notifications to topics, is called the **Publisher**. +End clients that subscribe to topics and then receive notifications are just **Clients**. -### [LeanPipe publisher](publisher/README.md) +The topics may require some arbitrary permissions from the clients that subscribe to them. -LeanPipe part that handles clients subscriptions to the topics and allows publishing to them. +Before subscribing to any topic the SignalR Connection to the Publisher is established by the Client. -### LeanPipe clients -The clients allow subscribing to various notification topics and receive real time, strongly typed notifications published by the publisher on the topic. +## LeanPipe core parts -Existing LeanPipe clients: +In order to setup real time notifications via LeanPipe in an app one would need a Publisher server and at least one of the provided clients. -1. Dart [TBD] -2. TypeScript [TBD] +### Publisher + +Handles clients subscriptions to the topics and allows publishing to them. + +Available as a library for monolithic architectures and also as a microservice friendly reverse proxy called the Funnel. + +See more [here](publisher/README.md). -## LeanPipe concepts +### Client SDKs -[TODO] +The clients allow subscribing to various notification topics and receive real time, strongly typed notifications published on the topic. + +Supported LeanPipe client SDKs: + +1. [Dart](dart/README.md) +2. TypeScript [TBD] diff --git a/publisher/README.md b/publisher/README.md index 7491e0f..c17c62d 100644 --- a/publisher/README.md +++ b/publisher/README.md @@ -1,9 +1,158 @@ # LeanPipe Publisher -![Build & Release](https://github.com/leancodepl/leanpipe/actions/workflows/publisher_cd.yml/badge.svg) -![Feedz](https://img.shields.io/feedz/v/leancode/public/LeanPipe) +![Build & Release](https://github.com/leancodepl/leanpipe/actions/workflows/publisher_release.yml/badge.svg) +![Feedz](https://img.shields.io/feedz/v/leancode/public/LeanCode.Pipe) [![codecov](https://codecov.io/gh/leancodepl/leanpipe/graph/badge.svg?token=LZIAEF100M)](https://codecov.io/gh/leancodepl/leanpipe) -Publisher part of the LeanPipe notification system not only publishes manages to topics, but also handles clients requests to get subscribed on a topic. +Publisher part of the LeanPipe notification system that handles client subscription and publishes notifications on the particular topics. -[TODO] +## Usage guide + +### Defining Topics & Notifications in contracts + +Define topics as DTOs implementing `ITopic` interface. Since clients subscribe to particular topic instances which must be distinguishable, one major requirement regarding topic DTOs appears: + +> [!IMPORTANT] +> Defined topics must only use properties that later could be compared using deep equality. + +If the topic is to require permissions, use [authorize attributes](https://github.com/leancodepl/contractsgenerator/blob/main/src/LeanCode.Contracts/Security/AuthorizeWhenAttribute.cs), as you would in your CQRS objects. + +Then, define notifications as simple DTOs. +In order to indicate which notifications can be published to topic, implement `IProduceNotification` in the topic. + +### LeanPipe Topics vs SignalR Groups + +Although we strive for the developer experience to feel frictionless and magical, we need to know a bit about SignalR groups. + +That’s because topics operate on the [SignalR groups](https://learn.microsoft.com/en-us/aspnet/core/signalr/groups?view=aspnetcore-8.0) underneath, which are quite limited. +You can add a connection to the group, you can remove it, and you can send a message to the group. +You cannot list them, check their connections, even check if they exist themselves. +Those properties are also topics instances properties. + +However, one topic instance may work on multiple groups, which gives some flexibility to the topic developer. +For that to work, one must implement a `ISubscribingKeys` and `IPublishingKeys` for each topic and it’s notification, which is responsible for mapping a topic instances and notifications to groups. +It should be easiest to maintain if all of those singular topic interfaces implementations would be in a single class. +That mapping will be used every time a client subscribes, unsubscribes or backend wants to publish a notification. + +Consider prefixing generated keys with with a prefix unique per topic, in order to dodge some inter-topic keys clashes +(it could make user unsubscribe from one topic instance while the client wanted to unsubscribe from just the other topic instance). + +### Publishing notifications + +Publishing from the publishers POV is quite simple really. +Just inject a `ILeanPipePublisher` in the handler of your choice, where `TTopic` is type of topic you’d like to publish to, construct a notification from the topic notification pool and the topic instance, `publisher.PublishAsync(topic, notification)` and you’re good to go. + +### Examples + +This library is designed having in mind use cases which would fall into two groups: + +1. Basic topic +2. Dynamic topic + +#### Basic topic example + +It suffices use cases which are very simple, however also very common (so we expect at least). +Let’s imagine a client would like to know about updates of a single, distinguishable entity - an auction, an import process, a match. +A something that a client would be interested to know about on just a single page in their app probably. +Let’s stick to the auction in this example. +The topic would be defined as such: + +```csharp +public class AuctionNotifications : ITopic, IProduceNotification, IProduceNotification +{ + public string AuctionId { get; set; } +} + +public class BidPlaced +{ + public int Amount { get; set; } + public string User { get; set; } +} + +public class ItemSold +{ + public string Buyer { get; set; } +} +``` + +For that basic scenario to work we expose you an abstract class which needs just a single method implementation - `BasicTopicKeys` with missing method `IEnumerable Get(TTopic topic)`. +Its implementation will be elementary: + +```csharp +AuctionNotificationsKeysFactory : BasicTopicKeys +{ + public IEnumerable Get(AuctionNotifications topic) + { + return new[] { topic.AuctionId.ToString() }; + } +} +``` + +And that’s all your clients may subscribe/unsubscribe to `AuctionNotifications` and you can publish as much as you want. + +#### Dynamic topic example + +Okay, this would probably a little bit more exotic, but still useful. +Let’s think about something a user might want to know about, but it could be different for each users, like `FollowedMatchesNotifications`, `MyAuctionsNotifications`, etc. +From the apps POV those topics would probably be useful in a lot of pages, possibly even everywhere on the app so the client could subscribe to on the app start. +Let’s stick to the `MyAuctionsNotifications` topic. +It could look quite like: + +```csharp +public class MyAuctionsNotifications : ITopic, IProduceNotification, IProduceNotification +{ } + +public class BidPlaced +{ + public string AuctionId { get; set; } + public int Amount { get; set; } + public string User { get; set; } +} + +public class ItemSold +{ + public string AuctionId { get; set; } + public string Buyer { get; set; } +} +``` + +This is a bit different from the example above, now we need context of the concrete auction in each notification. +Fret not, we’ve got you covered my dear future topic implementer. +However now the `BasicTopicKeys` will be of no use to us - we need to go deeper, we need to have a class that implements a `IPublishingKeys` for each type of notification from the topics pool, +which will be equivalent to providing implementation for `ValueTask> GetForSubscribingAsync(TTopic topic, LeanPipeContext context)` and each of `ValueTask> GetForPublishingAsync(TTopic topic, TNotication notification)`. +This is how it could look like: + +```csharp +MyAuctionsNotificationsKeys : IPublishingKeys, IPublishingKeys +{ + private DbContext dbContext; + + public async ValueTask> GetForSubscribingAsync(Auction topic, LeanPipeContext context) + { + var userId = context.HttpContext.GetUserId(); + + var usersAuctions = await dbContext.Auctions + .Where(a => a.SellerId == userId) + .Select(a => a.Id) + .ToListAsync(context.HttpContext.RequestAborted); + + return usersAuctions.Select(a => a.ToString()); + } + + public async ValueTask> GetForPublishingAsync(Auction topic, BidPlaced notification) + { + return Task.FromResult(new[] { notification.AuctionId.ToString() }); + } + + public async ValueTask> GetForPublishingAsync(Auction topic, ItemSold notification) + { + return Task.FromResult(new[] { notification.AuctionId.ToString() }); + } +} +``` + +That’s a little bit more than in the basic example, but also works! + +> [!WARNING] +> In this model the entity list is a snapshot from the subscription time. +> Should one want to have an updated snapshot they should resubscribe again.