Skip to content

Commit

Permalink
Servers and clients
Browse files Browse the repository at this point in the history
  • Loading branch information
steffansluis committed Mar 20, 2019
1 parent d50c5c4 commit 6ed49df
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 30 deletions.
90 changes: 65 additions & 25 deletions src/main/scala/chatapp/Chat.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import rover.rdo.AtomicObjectState
import rover.rdo.client.RdObject
import cats.implicits._
import com.monovore.decline._
import rover.Client.OAuth2Credentials
import rover.{Client, Server, Session}

import scala.concurrent.ExecutionContext.Implicits.global
import scala.async.Async.{async, await}
Expand All @@ -30,8 +32,9 @@ class ChatMessage(val body: String,

// FIXME: ensure messages can be read, but not modified or reassigned...
// FIXME: after state & rd object impl change
class Chat(_onStateModified: Chat#Updater) extends RdObject[List[ChatMessage]](AtomicObjectState.initial(List[ChatMessage]())) {
type Updater = AtomicObjectState[List[ChatMessage]] => Promise[Unit]
class Chat(_onStateModified: Chat#Updater, initialState: AtomicObjectState[List[ChatMessage]] = AtomicObjectState.initial(List[ChatMessage]())) extends RdObject[List[ChatMessage]](AtomicObjectState.initial(List[ChatMessage]())) {

This comment has been minimized.

Copy link
@giannislelekas

giannislelekas Mar 20, 2019

Collaborator

The initialState shouldn't be List[AtomicObjectState[ChatMessage]]?
This way it contradicts to our AtomicState and Log implementation.

This comment has been minimized.

Copy link
@steffansluis

steffansluis Mar 20, 2019

Author Owner

The top level should always be AtomicObjectState right? It is AtomicObjectState[List[ChatMessage]] atm, but that is placeholder anyway, just to make it work. I had to do some hackery with the RdObject anyway, having to do with abstract/non-abstract classes, but basicaly consider this unfinished and subject to change.

This comment has been minimized.

Copy link
@giannislelekas

giannislelekas Mar 20, 2019

Collaborator

This basically boils down to keep the last element of the list as the current immutable state and append the others to the log. I'm making some changes now, as soon as I got something working I'll push so you can check what I mean.

This comment has been minimized.

Copy link
@PhilipeLouchtch

PhilipeLouchtch Mar 20, 2019

Collaborator

List of Messages as state makes sense, I would have created a class "[Chat]Messages" but can refactor later

This comment has been minimized.

Copy link
@steffansluis

steffansluis Mar 20, 2019

Author Owner

Yea I abstracted it away as a ChatState class at first, but I found myself proxying all of its methods on the Chat class so I decided to focus on getting something working first

// type State = List[ChatMessage]
type Updater = AtomicObjectState[List[ChatMessage]] => Future[Unit]

def send(message: ChatMessage): Promise[Unit]= {
val op: AtomicObjectState[List[ChatMessage]]#Op = s => s :+ message
Expand All @@ -41,7 +44,7 @@ class Chat(_onStateModified: Chat#Updater) extends RdObject[List[ChatMessage]](A
}
}

override def onStateModified(oldState: AtomicObjectState[List[ChatMessage]]): Promise[Unit] = {
override def onStateModified(oldState: AtomicObjectState[List[ChatMessage]]): Future[Unit] = {
_onStateModified(state)
}

Expand All @@ -50,27 +53,50 @@ class Chat(_onStateModified: Chat#Updater) extends RdObject[List[ChatMessage]](A
}

}

object Chat {
val SIZE = 10
def fromRDO(rdo: RdObject[List[ChatMessage]], _onStateModified: Chat#Updater): Chat = {
new Chat(_onStateModified, rdo.state)
}
}

def client(serverAddress: String, user: User): Unit = {
val printer = (string: String) => {
val cls = s"${string.split("\n").map(c => s"${REPL.UP}${REPL.ERASE_LINE_BEFORE}${REPL.ERASE_LINE_AFTER}").mkString("")}"
// Prepend two spaces to match input indentation of "> "
val text = string.split("\n").map(line => s" $line").mkString("\n")
class ChatServer() extends Server[OAuth2Credentials](null) {

s"${REPL.SAVE_CURSOR}$cls\r$text${REPL.RESTORE_CURSOR}"
}
}

class ChatClient(serverAddress: String) extends Client.OAuth2Client(serverAddress, (credentials: OAuth2Credentials) => credentials.accessToken) {
var session: Session[OAuth2Credentials] = null
var user: User = null
var chat: Chat = null;

val printer = (string: String) => {
val cls = s"${string.split("\n").map(c => s"${REPL.UP}${REPL.ERASE_LINE_BEFORE}${REPL.ERASE_LINE_AFTER}").mkString("")}"
// Prepend two spaces to match input indentation of "> "
val text = string.split("\n").map(line => s" $line").mkString("\n")

s"${REPL.SAVE_CURSOR}$cls\r$text${REPL.RESTORE_CURSOR}"
}

// TODO: This is hacky, figure out a better way to do this
val updater: Chat#Updater = state => Promise() completeWith async {
val text = state.immutableState.takeRight(SIZE).map(m => m.toString()).mkString("\n")
print(s"${printer(text)}")
val updater: Chat#Updater = state => async {
val text = state.immutableState.takeRight(ChatClient.SIZE).map(m => m.toString()).mkString("\n")
print(s"${printer(text)}")
}

def login(user: User): Future[Unit] = {
async {
val credentials = new OAuth2Credentials("fake credentials", "fake credentials")
this.user = user
session = createSession(credentials)
val rdo = await(session.importRDO[List[ChatMessage]]("chat"))
chat = Chat.fromRDO(rdo, updater)
}
}

val chat = new Chat(updater)
def render(): Future[Unit] = {
// val chat = new Chat(updater)
println(" Welcome to Rover Chat!")
print((1 to SIZE).map(i => "\n").mkString(""))
print((1 to ChatClient.SIZE).map(i => "\n").mkString(""))

// Simulate conversation
async {
Expand All @@ -93,23 +119,37 @@ object Chat {
s
}
val executor = (input: String) => {
async {
val p = chat.send(new ChatMessage(input, user))
await(p.future)
// This clears the input line
print(s"${REPL.UP}${REPL.ERASE_LINE_AFTER}")
chat.immutableState.takeRight(SIZE).map(m => s"${m.toString()}").mkString("\n")
}
async {
val p = chat.send(new ChatMessage(input, user))
await(p.future)
// This clears the input line
print(s"${REPL.UP}${REPL.ERASE_LINE_AFTER}")
chat.state.immutableState.takeRight(ChatClient.SIZE).map(m => s"${m.toString()}").mkString("\n")
}
}
val repl: REPL[String] = new REPL(reader, executor, printer)
Await.result(repl.loop(), Duration.Inf)
// Await.result(repl.loop(), Duration.Inf)
repl.loop()
}

}

object ChatClient {
val SIZE = 10

def main(args: Array[String]): Unit = {
client("bla", User.Steffan)
val serverAddress = "bla"
val client = new ChatClient(serverAddress)
val f = async {
await(client.login(User.Steffan))
await(client.render())
}

Await.result(f, Duration.Inf)
}
}


// TODO: Add client and server subcommands
object ChatCLI extends CommandApp(
name = "rover-chat",
Expand Down
19 changes: 19 additions & 0 deletions src/main/scala/rover/Client.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package rover

class Client[C](serverAddress: String, identifier: Session[C]#Identifier) {
val server = Server.fromAddress[C](serverAddress)

def createSession(credentials: C) = {
server.createSession(credentials)
}
}


object Client {
class OAuth2Credentials(val accessToken: String, val refreshToken: String) {}
type OAuth2Client = Client[OAuth2Credentials]

def oauth2(): OAuth2Client = {
null
}
}
18 changes: 18 additions & 0 deletions src/main/scala/rover/Server.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package rover

class Server[C](identifier: Session[C]#Identifier) {
// TODO: Determine what to do with this
def clientFromCredentials(credentials: C): Client[C] = {
null
}

def createSession(credentials: C) = {
new Session[C](credentials, this, clientFromCredentials(credentials))
}
}

object Server {
def fromAddress[C](address: String): Server[C] = {
new Server[C](null)
}
}
47 changes: 47 additions & 0 deletions src/main/scala/rover/Session.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package rover

import rover.rdo.AtomicObjectState
import rover.rdo.client.RdObject

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.async.Async.{async, await}

class Session[C](credentials: C, server: Server[C], client: Client[C]) {
type Id = String
type Identifier = C => Id

// TODO: Move this to the proper place
type ObjectId = Id

// val server: Server[C] = null
// val client: Client[C] = null

// TODO: Implement errors
def importRDO[T](objectId: ObjectId): Future[RdObject[T]] = {
// TODO: Hacks
async {
if (objectId == "chat") new RdObject[T]( AtomicObjectState.initial(List[Any]().asInstanceOf[T]) )
else null
}
}

def exportRDO[T](rdo: RdObject[T]): ObjectId = {
null
}


}

object Session {
// TODO: Probably better to make this mutable? It needs to be persistent in any case
private var _CACHE = Map[Session[Any]#Id, Session[Any]]()

def get[T](sessionId: Session[T]#Id): Session[T] = {
_CACHE.get(sessionId).asInstanceOf[Session[T]]
}

def set[T](sessionId: Session[T]#Id, session: Session[T]): Unit = {
_CACHE = _CACHE.updated(sessionId, session.asInstanceOf[Session[Any]])
}
}
15 changes: 10 additions & 5 deletions src/main/scala/rover/rdo/client/RdObject.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import rover.rdo.AtomicObjectState

import scala.concurrent.ExecutionContext.Implicits.global
import scala.async.Async.{async}
import scala.concurrent.{Promise}
import scala.concurrent.{Promise, Future}

//FIXME: use hashes instead of Longs/Strings?
abstract class RdObject[A](var state: AtomicObjectState[A]) {
class RdObject[A](var state: AtomicObjectState[A]) {

// TODO: "is up to date" or "version" methods

Expand All @@ -16,7 +16,11 @@ abstract class RdObject[A](var state: AtomicObjectState[A]) {
// onStateModified(state)
}

protected def onStateModified(oldState: AtomicObjectState[A]): Promise[Unit]
protected def onStateModified(oldState: AtomicObjectState[A]): Future[Unit] = {
async {

}
}

protected final def immutableState: A = {
return state.immutableState
Expand All @@ -36,8 +40,9 @@ abstract class RdObject[A](var state: AtomicObjectState[A]) {
* @param other Some other RDO
*/
class CommonAncestor[A](private val one: RdObject[A], private val other: RdObject[A]) extends RdObject[A](null) { // todo: fixme with a deferred state
override def onStateModified(oldState: AtomicObjectState[A]): Promise[Unit] = {
Promise() completeWith async { }
override def onStateModified(oldState: AtomicObjectState[A]): Future[Unit] = {
// Promise() completeWith async { }
async {}
}

// determine it once and defer all RdObject methods to it
Expand Down

0 comments on commit 6ed49df

Please sign in to comment.