Patterns and options for sharing mutable state between routes?

Hi! I'm attempting to build a (very simple) proof-of-concept Vapor server that delivers a GET API along with a websocket API. My goal is for clients to connect to a websocket and receive notifications when something interesting happens on the server.

My goal (for now) with a GET endpoint is just an echo… I will just have a simple hello world GET endpoint that broadcasts that message out to the "current" websockets.

Something I'm not completely clear on is what options should I consider for sharing some kind of state between a GET route and a websocket rote. My vapor server needs some way to "message back" from the GET route to the websockets that are active.

Here is an attempt at this:

import Vapor

final actor Registrar {
  private var array = Array<WebSocket>()
  
  func append(_ ws: WebSocket) {
    self.array.append(ws)
  }
  
  func send<S>(_ text: S) async throws where S: Sendable, S: Collection, S.Element == Character {
    for ws in self.array {
      try await ws.send(text)
    }
  }
}

func main() async throws {
  let app = try await Application.make(.detect())
  let registrar = Registrar()
  app.get("hello", "vapor") { req in
    try await registrar.send("Hello, vapor!")
    return "Hello, vapor!"
  }
  app.webSocket("echo") { req, ws in
    await registrar.append(ws)
    ws.onClose.whenComplete { result in
      //  TODO: REMOVE FROM REGISTRAR
    }
  }
  try await app.execute()
  try await app.asyncShutdown()
}

try await main()

This code seems to be doing the right thing (so far)… other than the TODO that I should remove the socket from the stack after the client closes the connection.

Is there any other pattern here the community likes for this problem? I can keep hacking on this approach… but I'm also open to any other ideas or patterns that might be out there for something similar. This does not have to be production scale for my use-case… but I'm open to migrating to a production scale solution if the code is also quick and easy. Thanks!

The approach you've landed on here seems totally right to me.

2 Likes

Hmm… I'm looking through the Vapor documentation and I believe it is also supported to share mutable state (like this Registrar instance) across the Vapor.Application and Vapor.Request instances. I believe this would imply refactoring my sample code to deliver a singleton Registrar instance through extensions on Vapor.Application and Vapor.Request. Is this approach usually what you see more often in the real-world at scale?

It depends. Services in Vapor have a slight hangover from the EventLoop days because you had to (or at least should) tie everything happening on a request to a single EventLoop. Which is why you had the same properties across the Application and Request (to enable the request Service got the request's EventLoop). That breaks down a bit in the Swift Concurrency world and isn't really necessary anymore (unless you still need an event loop).

Additionally, for your use case where you want a shared cache across all of the requests, you'd need to manage that (and have locks), so using an actor is the correct approach here. You could attach your registrar to the application's Storage to make it accessible easily but honestly standard dependency injection is much cleaner and easier to work with and will be the approach for Vapor 5 so I'd just stick with that.

1 Like