Here is the story, Sodium is more than brilliant.
It takes away the pain.
I decided to do all my state handling with Sodium on the client side.
Here is the github link, if you wanna see the code right away.
It's work in progress.
Long story short, here is how I will implement a relational database on the client side, and ditch all SQL in an SPA (nothing on server side either) - and the whole story is super dead simple.
So, I have database rows, conceptually, but they are represented as classes.
Each database row, has a value
and a primary key
.
For example this is how the value
part looks like for a "database table" called Note
:
case class Note(
title: String,
content: String,
owner: EntityIdentity[User])
extends EntityType[Note]
This is a VALUE in a "database table", which has a foreign key reference to a User
entity ( Entity
is a synonym to a database table).
The most important part of the story is, that Entity
-s are normalized. What I will be describing now is a fully normalized representation of the data, and how Sodium will be used to create denormalized representations, queries.
More importantly, if you update the normalized part of the "database", then Sodium updates the denormalized values, AUTOMATICALLY.
But is this good at all ?
Because the "denormalize values", are stored in Sodium Cell's which correspond to a React's Component State, and I set up a listener to change the Component's State when the denormalized state will change using cell.listen(s=> setComponentState(s))
.
So a change in a denormalized view (some Sodium Cell which depends on the normalized "database" representation of the whole state of the app) will cause the VDOM to change. So the VDOM will be always updated to show the latest representation of the denormalized values.
On top of this, since I am using React, this will be fast (AND COMPOSITIONAL, since VDOM is IMMUTABLE).
It will be fast because react only updates as little as possible in the real DOM which actually corresponds to what is displayed in the browser. React does this by comparing the old VDOM (before the change in the normalized "database tables") with the new VDOM (after the change).
So, here is the code example that shows the implementation :
The database table is represented as :
case class EntityWithRef[E <: EntityType[E]](
entityValue: E,
toRef: RefToEntityWithVersion[E])
where RefToEntityWithVersion[E]
is :
case class RefToEntityWithVersion[T <: EntityType[T]](
entityValueTypeAsString: EntityValueTypeAsString,
entityIdentity: EntityIdentity[T] = EntityIdentity[T](),
entityVersion: EntityVersion = EntityVersion())
and
case class EntityIdentity[V<:EntityType[V]](uuid: String=java.util.UUID.randomUUID().toString){
def stripType:EntityIdentityUntyped=EntityIdentityUntyped(uuid)
}
So basically a Map[EntityIdentity[V], EntityWithRef[V]]
is a fully NORMALIZED database table.
I store this map in a Sodium Cell, for each entity type (V<:EntityType[V]
), there will be a Cell like this:
Cell[Map[EntityIdentity[V], EntityWithRef[V]]
This cell will be wrapped into some helper class, for not relevant reasons.
The Cell's will be populated at the start of the SPA, from the server.
Then if I want to create any kind of denormalized view from the completely normalized data representations (a Cell[Map[EntityIdentity[V], EntityWithRef[V]]
for each type of V
, then I can calculate those denormalized representations using Sodium, filters, joins, whatever I fancy.
Then the denormalize view will be linked up to a React Component, like this :
case class SodiumLabel(val c: Cell[String]) {
def sampledValue = c.sample()
var state = "initial_state"
var stateSetter: ReRenderTriggerer = ReRenderTriggerer(None)
val f = c.listen(x => {
println(s"sodum label's cell is $x");
state = x
stateSetter.trigger()
})
val comp = ScalaComponent
.builder[Unit]("SodiumLabel")
.initialState("label")
.renderBackend[Backend]
.componentWillMount(f => {
val g = () => f.setState("bla").runNow()
Callback {
val s = ReRenderTriggerer(Some(g))
stateSetter = s
}
})
.build
class Backend($ : BackendScope[Unit, String]) {
def render(s: String) = {
<.div(
state
)
}
}
}
Here is the github link for this code ^.
Then I can create a piece of VDOM like this:
object FRP {
val sbutton = SodiumWidgets.SodiumButtom()
val cell: Cell[String] =
sbutton.sClickedSink.map(x => "bello").hold("hello")
val label: SodiumWidgets.SodiumLabel =SodiumWidgets.SodiumLabel(cell)
val vdom= <.div(
sbutton.getVDOM(),
<.br,
label.comp()
)
}
Where vdom
represents what will be drawn onto the screen. In this case label.comp()
will draw whatever text the Cell[String]
holds, which was used to create the SodiumLabel
Sodium Component, which gives a STATEFUL React Component Constructor when I call label.comp()
.
label.comp()
is a React Component Constructor which is used by React to construct an actual stateful React Component which is used to describe an algorithm, in a compositional, hierarchical way, that generates a VDOM, this is what React does. It uses the "algorithm" which is assembled together by using Stateful React Components and plain HTML elements.
What React does, is, it takes your algorithm, which has state in it and when you call render()
, then it displayes the VDOM generate by your algorithm (which is stateful).
render()
needs to be called every time the state in your algorithm that produces the VDOM changes (such state change usually happens in response to some events, AJAX call returns, somebody sends you a message view TCP/WebSocket, time ticker ticks, mouse click, button press, etc...).
There is one final part to the story. Every time the normalized data changes. (which are stored in the client as Cell[Map[EntityIdentity[V], EntityWithRef[V]]
-s. That change will be propagated to the server and the changes will be saved into a journal. So, if you wish, you can replay all the changes from the beginning of time, but the Cell
's on the client side will receive always the latest version of the Entities. So the client side will only know about the latest state of the app's history, the server will remember every change. So, the server will store the data in an immutable way.
This architecture makes it possible to use CQRS and other event based / Stream based calculations on the server.
The server can send an update to the client's normalized data if somebody else changes that normalized data on the server, for example a Cell[List[String]]
that contains the messages in a chat room, and this state is stored on the server. A client can send a message to the server to change this Cell[List[String]]
and other clients will get automatically an event to update their
Cell[List[String]]` -s , which is stored locally on the client side, in my case, using the Scala.js version of Sodium.
This is work in progress, and I know that Sodium has some memory leak issues in the Scala implementation, but according to @the-real-blackh those issues can be fixed whenever they need to be fixed. So, the memory leak, which is currently a problem with Scala Sodium is not gonna be dealbreaker, because it can be fixed, @the-real-blackh already explained how, in some other thread, but I will do that once I have a prototype of a working app which starts to leak memory... then I need to start to fix that memory leak issue which is present only in Scala Sodium when using it with Scala.js. Scala Sodium when used on the JVM has no such problem, but Scala.js might have it... according to @the-real-blackh, well, if I start to run into memory leaks in my App, then @the-real-blackh has already explained to me how those can be fixed, so I am not worried about that being a dealbreaker.
So, this is my plan, I started yesterday, because I hated the current solution. I had something similar, but not reactive, not compositional, not declarative, not stream based, a real nightmare and pain. After the app started to be even a slightly bit more complex than displaying a list of User
-s and their Note
-s, I decided it's time to end this suffering, throw out that mess and replace the state handling logic with Sodium Scala , I think it will be at most one week of work to get a first working version. I have lot's of code in place already, and before it's too late, I throw out the current state handling logic ( which started to look very disgusting yesterday ). I will replace it with Sodium Scala, and in theory, according to the plan that I described here, it should be pretty awesome, in theory, let's see what practice will bring.
I will update this thread as I progress.