- Notifications
You must be signed in to change notification settings - Fork1
CodeMettle/jsactor
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Actors on Scala.js
Check out a rough example project athttps://github.com/CodeMettle/jsactor-spa-example
Much of this code (the good parts) are straight fromTypesafe/Akka and I don't claimauthorship. Most of the code that isn't straight from Akka was at least written while looking at Akka code and usestheir concepts and techniques. Anything broken is my fault, everything else is thanks to their work. My hope is thatthey create and maintain an official Scala.js version of Akka; until thenjsactor
seems to mostly work.
While this projects works, somewhat, it's not close to production-quality.
I'm used to working with the Actor model on the backend, with Scala.js it feels natural to use Actors and havehierarchy, a well-defined lifecycle, anEventStream
, and so on.
libraryDependencies++=Seq("com.codemettle.jsactor"%%%"jsactor"%"0.8.0","com.codemettle.jsactor"%%%"jsactor-bridge-client"%"0.8.0"// if using jsactor-bridge"com.codemettle.jsactor"%%%"jsactor-loglevel"%"0.8.0"// to use the LogLevel logging adapter)
(deleted JsActor section since it's now gone in favor of Akka.JS)
This is the part we care about, right? Why use Actors on the client if we're not interacting with a backend ActorSystem?
To that end, I'm including some extra projects that make interaction pretty seamless, at least in our usage. We havemany µservices running on the backend, one of which consolidates large amounts of data from many different sources andtransforms to model objects and pushes changes to the GUI. No pull, no REST, no client-side polling for changes - thebackend reacts to changes it cares about, based on the current client view (what it cares about), and when the modelobjects change then the server pushes them.
// project with model classes shared between client & server, cross-compiled to JVM & JS ...libraryDependencies+="com.codemettle.jsactor"%%%"jsactor-bridge-shared"%"0.8.0" ...
Currently this project (so, by extension, client and server bridge projects) depends onuPickle, with no way to change that.
This whole readme is out of date, but nobody uses this library anyway, so NBD. But there now are*-upickle
projectsthat are based on uPickle. I aim to addBooPickle and/orCirce implementations of client-server bridges.
uPickle
was written, I assume, withautowire in mind - all types are known inadvance. That presents an issue forjsactor
since the Akka model is typeless and depends on pattern matching.I worked around this with inspiration from (or, really, by copying)scala-js-pickling'sPicklerRegistry
. The top-level bridge actor on client and server need an implicitBridgeProtocol
in scope. Theprotocol has to register every top-level message that is going to be sent across the bridge.
objectMyProjectProtocolextendsBridgeProtocol {overridedefregisterMessages(registry:MessageRegistry):Unit= {defadd[A:Reader:Writer:ClassTag]= registry.add[A]defaddObj[A<:Singleton:Reader:Writer:ClassTag](obj:A)= registry.addObj(obj) addObj(SessionApi.SubscribeToUserAndAlarmPrefs) add[SessionApi.UserUpdate] add[SessionApi.PreferenceUpdate] add[SessionApi.UpdatePreferences] add[SessionApi.PreferenceUpdateResult] }}
// server project; also needs to depend on the shared model project ...libraryDependencies+="com.codemettle.jsactor"%%%"jsactor-bridge-server"%"0.8.0" ...
The server-side of the bridge takes a reference to an Actor which sends/receives Strings to/from the client,presumably over a WebSocket.
classServerBridgeActor(clientWebSocket:ActorRef)(implicitbridgeProtocol:BridgeProtocol)extendsActor
Play 2.3 happens to give us an Actor representing a WebSocket, so this is pretty trivial in Play:
objectMyControllerextendsController { ...privatedefgetSession(implicitrequest:RequestHeader):Future[Option[ClientSession]]=???defws=WebSocket.tryAcceptWithActor[String,String] { request⇒ getSession(request) map {caseNone⇒Left(Forbidden)caseSome(session)⇒implicitvalbridgeProtocol=MyProjectProtocolRight((wsActor)⇒ServerBridgeActor.props(wsActor)) } } ...}
// client project; also needs to depend on the shared model project ...libraryDependencies+="com.codemettle.jsactor"%%%"jsactor-bridge-client"%"0.8.0" ...
jsactor-bridge-client
provides building blocks for creating the client-side of a bridge, any/all of the providedclasses can be replaced with your own, but it's easiest to usejsactor.bridge.client.{ClientBridgeActor, SocketManager, WebSocketActor}
.
valactorSystem:ActorSystem=???privateimplicitvalbridgeProtocal=MyProjectProtocolvalwsManager= actorSystem.actorOf(SocketManager.props(SocketManager.Config("ws://localhost:9000/ws"),"socketManager")classMyActorextendsActor {overridedefpreStart()= {super.preStart() wsManager!SocketManager.Events.SubscribeToEvents }overridedefreceive= {caseSocketManager.Events.WebSocketConnected(socket)⇒ socket!WebSocketActor.Messages.SendMessageToServer("/user/MyServerActor",MessageThatIHaveRegistered()) socket!WebSocketActor.Messages.SendMessageToServer("akka.tcp://ActorSystem@address:port/user/MyServerActor",MessageThatIHaveRegistered())// could receive a jsactor.bridge.protocol.ServerActorNotFound message,// otherwise the server-side actor will have a proxy representing this client-side actor, any messages the// server sends to that actor will come to this actor; the server can DeathWatch that proxy to be notified// that this actor has shutdown (or the WebSocket was interrupted) socket!WebSocketActor.Messages.IdentifyServerActor("/user/MyServerActor")// this actor will receive a jsactor.bridge.protocol.ServerActorNotFound message, or will receive a// jsactor.bridge.protocol.ServerActorFound message (sent from the client-side proxy of that server-side actor).// Any message sent to this client-side proxy will be sent to the corresponding server-side actor, and the// client-side proxy can be DeathWatched to notify that the server actor has shutdown (or WebSocket was interrupted). } }
I've also included a rough utility trait,jsactor.bridge.client.util.RemoteActorListener
, that can be used toautomatically start trying toIdentify
a server-side actor when the websocket connects (or reconnects):
importjsactor.bridge.client.util.RemoteActorListenerclassMyClientActor(valwsManager:ActorRef)extendsRemoteActorListener {defactorPath="/user/MyServerActor"defonConnect(serverActor:ActorRef)= { serverActor!MyRegisteredMessage }defwhenConnected(serverActor:ActorRef)= {caseRegisteredMessageFromServer(param)=> log.info(s"got$param from server")case msg@RegisteredMessageFromClient=> serverActor forward msg }}