SaturnFramework / Saturn

Opinionated, web development framework for F# which implements the server-side, functional MVC pattern
https://saturnframework.org
MIT License
714 stars 108 forks source link

[Channels] Sending server-side messages #171

Closed baronfel closed 5 years ago

baronfel commented 5 years ago

With channels merged, we have a nice way to trigger server-side changes based on client-sent websocket messages, but it would be really nice to also be able to send messages from the server to a particular channel easily. I'm going to lay out a simple wishlist here of what I'd like to be able to do and then a short design.

Needs:

I think a decent mechanism for this would be to wrap the current channel state dictionary in a type that can be used to provide these methods, then register that type with the AspNetCore DI system for use in the rest of the server.

Speclet:


type SubscriberInfo = 
  { Id: Guid 
    IP: IpAddress }

type ChannelHub() =
  member x.SendAll(topic: string, msg: 'a): Task<unit>
  member x.SendSubscriber(topic: string, subscriber: Guid, msg: 'a): Task<unit>
  member x.Subscribers(): Task<Map<string, SubscriberInfo list>>
  member x.SubscribersForTopic(topic: string): Task<SubscriberInfo list>
  member x.Subscriber(topic: string, id: Guid): Task<SubscriberInfo option>

And the ChannelHub would be injected into the DI context at startup, and be backed by the internal sockets dictionary (or some derivative of that).

So in a handler we could do:


let saveThing next ctx = task {
  let! thing = ctx.BindModel<Thing>()
  let! saveResult = saveThingToDb thing
  let hub = ctx.ResolveService<ChannelHub>()
  let msg: NewThingMsg = { Id: saveResult.Id }
  do! hub.SendAll("thing.new", msg)
  return! setStatusCode 201 next ctx
}

An initially-untyped interface seems fine, because if a user wanted they could easily build a typed-hub on top of this:

type Hub<'msg>(hub: ChannelHub, topic: string) =
  member x.Send(m: 'msg) = hub.SendAll(topic, m)
TheAngryByrd commented 5 years ago

SubscriberInfo should probably hold the HttpContext of the request that initially started the websocket. I know that's kind of dangerous but pulling off all the possible things a user would want might be tricky (Things like User if authenticated, IPAddress, etc). We could put some members on there to make apparent the user shouldn't access that HttpContext directly but I wouldn't want to hide it entirely.

baronfel commented 5 years ago

Yeah I was thinking we might want it, but deliberately leaning on being conservative for the first pass. Worst case you could make the subscriberinfo type user-defined/generic and let them pass a generation-function to make the subscriberinfo

TheAngryByrd commented 5 years ago

Additionally this gets into dotnet websockets aren't threadsafe territory. We can either steal more from https://github.com/TheAngryByrd/FSharp.Control.WebSockets/tree/master/src or reference it. It needs a bit of love though.

baronfel commented 5 years ago

Sorta-related: channel topics in Phoenix are somewhat used for routing. They can be of the following forms:

Is this something that we want to explicitly support? right now the wildcard matching isn't a thing from what I can read.

Krzysztof-Cieslak commented 5 years ago

Fixed by #174