This commit is contained in:
Julian 2025-07-12 14:02:54 +02:00
parent 263a609084
commit 40ea76895e
10 changed files with 149 additions and 36 deletions

View file

@ -4,7 +4,7 @@
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite --host",
"build": "vite build", "build": "vite build",
"preview": "vite preview" "preview": "vite preview"
}, },

View file

@ -12,10 +12,16 @@ import scala.concurrent.Future
import scala.scalajs.js import scala.scalajs.js
import model.Entry import model.Entry
import model.EntryId
import rdts.base.Lattice
import rdts.datatypes.LastWriterWins
import scala.scalajs.js.Date
import scala.util.Failure
import scala.util.Success
object DexieDB { object DexieDB {
private val schemaVersion = 1.0 private val schemaVersion = 1.1
private val dexieDB: Dexie = new Dexie.^("fahrtenbuch") private val dexieDB: Dexie = new Dexie.^("fahrtenbuch")
dexieDB dexieDB
@ -31,9 +37,62 @@ object DexieDB {
val entriesObservable: Observable[Future[Seq[Entry]]] = val entriesObservable: Observable[Future[Seq[Entry]]] =
liveQuery(() => getAllEntries()) liveQuery(() => getAllEntries())
def insertEntry(entry: Entry): Future[Any] = { def getEntry(id: EntryId): Future[Option[Entry]] =
println(s"inserting Entry $entry") entriesTable
entriesTable.put(entry.toNative).toFuture .get(id.delegate)
.toFuture
.map(_.toOption.map(NativeConverter[Entry].fromNative(_)))
def insertEntry(entry: Entry): Unit = {
val e: Future[Option[Entry]] = getEntry(entry.id)
e.flatMap {
case Some(oldEntry) =>
println(s"found old: $oldEntry")
println(s"found new: $entry")
println(oldEntry.id == entry.id)
val newEntry = Lattice[Entry].merge(entry, entry)
val newEntry2 = Lattice[Entry].merge(oldEntry, oldEntry)
println(oldEntry.id)
println(entry.id)
val test =
Lattice[Entry].merge(
Entry(
EntryId("1"),
LastWriterWins.now(0),
LastWriterWins.now(2),
LastWriterWins.now(""),
LastWriterWins.now(false),
LastWriterWins.now("Dirk"),
LastWriterWins.now(new Date())
),
Entry(
EntryId("1"),
LastWriterWins.now(0),
LastWriterWins.now(2),
LastWriterWins.now(""),
LastWriterWins.now(false),
LastWriterWins.now("Dirk"),
LastWriterWins.now(new Date())
)
)
val newEntry3 =
Lattice[Entry].merge(oldEntry, entry.copy(id = oldEntry.id))
println("yolo")
Future.unit
// entriesTable.put(newEntry.toNative).toFuture
case _ =>
entriesTable.put(entry.toNative).toFuture
}.onComplete {
case Failure(exception) => println(s"failed with $exception")
case Success(value) => ()
}
// .toFuture
// .map(e =>
// if e.isUndefined then entriesTable.put(entry.toNative).toFuture
// else
// val dbEntry = NativeConverter[Entry].fromNative(e)
// Lattice[Entry].merge(entry, dbEntry)
// )
} }
def getAllEntries(): Future[Seq[Entry]] = { def getAllEntries(): Future[Seq[Entry]] = {

View file

@ -13,11 +13,11 @@ import components.AppComponent
@main @main
def Fahrtenbuch(): Unit = def Fahrtenbuch(): Unit =
val appComponent = AppComponent(Main.allEntries) val appComponent = AppComponent(Main.allEntries, Trystero.onlineStatus)
renderOnDomContentLoaded( renderOnDomContentLoaded(
dom.document.getElementById("app"), dom.document.getElementById("app"),
AppComponent(Main.allEntries).render() appComponent.render()
) )
object Main { object Main {

View file

@ -1,6 +1,7 @@
package fahrtenbuch package fahrtenbuch
import com.raquo.laminar.api.L.* import com.raquo.laminar.api.L.*
import org.scalajs.dom
import org.scalajs.dom.RTCConfiguration import org.scalajs.dom.RTCConfiguration
import org.scalajs.dom.RTCIceServer import org.scalajs.dom.RTCIceServer
import org.scalajs.dom.RTCPeerConnection import org.scalajs.dom.RTCPeerConnection
@ -12,6 +13,10 @@ import typings.trystero.mod.joinRoom
import typings.trystero.mod.selfId import typings.trystero.mod.selfId
import scala.scalajs.js import scala.scalajs.js
import typings.trystero.mod.ActionProgress
import typings.trystero.mod.ActionSender
import typings.trystero.mod.ActionReceiver
import model.Entry
object Trystero: object Trystero:
private val eturn = new RTCIceServer: private val eturn = new RTCIceServer:
@ -35,11 +40,13 @@ object Trystero:
} }
// Public API // Public API
val room: Room = joinRoom(MyConfig, "fahrtenbuch") val roomId = dom.window.location.hash
val peerList: Var[List[(String, RTCPeerConnection)]] = Var(List.empty) val room: Room = joinRoom(MyConfig, roomId)
println(s"joining room $roomId")
val userId: Var[String] = Var(selfId) val userId: Var[String] = Var(selfId)
// listen for incoming messages // track online peers
val peerList: Var[List[(String, RTCPeerConnection)]] = Var(List.empty)
def updatePeers(): Unit = def updatePeers(): Unit =
peerList.set(room.getPeers().toList) peerList.set(room.getPeers().toList)
println(s"my peer ID is $selfId") println(s"my peer ID is $selfId")
@ -51,3 +58,14 @@ object Trystero:
println(s"$peerId left") println(s"$peerId left")
updatePeers() updatePeers()
) )
val onlineStatus: Signal[Boolean] = peerList.signal.map(_.nonEmpty)
object Actions:
// setup actions
private val entryAction: js.Tuple3[ActionSender[js.Any], ActionReceiver[
js.Any
], ActionProgress] = Trystero.room.makeAction[js.Any]("entry")
private val trysteroReceiveEntry: ActionReceiver[js.Any] = entryAction._2
def sendEntry(entry: Entry): Unit =
entryAction._1(entry.toNative)

View file

@ -0,0 +1,7 @@
package fahrtenbuch
import com.raquo.laminar.api.L.*
import fahrtenbuch.model.Entry
object Sync:
val entrySyncOut =
Observer[Entry](onNext = Actions.sendEntry(_))

View file

@ -1,17 +1,19 @@
package fahrtenbuch.components package fahrtenbuch.components
import com.raquo.laminar.api.L.* import com.raquo.laminar.api.L.*
import fahrtenbuch.model.Entry import fahrtenbuch.model.{Entry, EntryId}
import fahrtenbuch.Main.entryEditBus import fahrtenbuch.Main.entryEditBus
import rdts.base.Uid
class AppComponent(allEntries: Signal[Set[Entry]]): class AppComponent(
allEntries: Signal[Set[Entry]],
onlineStatus: Signal[Boolean]
):
// tracks whenever a user clicks on an edit button // tracks whenever a user clicks on an edit button
val editClickBus = new EventBus[(Uid, Boolean)] val editClickBus = new EventBus[(EntryId, Boolean)]
// tracks which entries are currently being edited // tracks which entries are currently being edited
val editStateSignal: Signal[Map[Uid, Boolean]] = val editStateSignal: Signal[Map[EntryId, Boolean]] =
editClickBus.stream.foldLeft(Map.empty[Uid, Boolean]) { editClickBus.stream.foldLeft(Map.empty[EntryId, Boolean]) {
case (acc, (id, value)) => case (acc, (id, value)) =>
acc + (id -> value) acc + (id -> value)
} }
@ -37,7 +39,7 @@ class AppComponent(allEntries: Signal[Set[Entry]]):
def render(): HtmlElement = def render(): HtmlElement =
div( div(
cls := "app content", cls := "app content",
h1("Fahrtenbuch"), h1("Fahrtenbuch", OnlineStatusComponent(onlineStatus).render()),
table( table(
cls := "table", cls := "table",
thead( thead(

View file

@ -1,16 +1,15 @@
package fahrtenbuch.components package fahrtenbuch.components
import fahrtenbuch.model.Entry import fahrtenbuch.model.{Entry, EntryId}
import com.raquo.laminar.nodes.ReactiveHtmlElement import com.raquo.laminar.nodes.ReactiveHtmlElement
import org.scalajs.dom.HTMLTableRowElement import org.scalajs.dom.HTMLTableRowElement
import com.raquo.laminar.api.L.* import com.raquo.laminar.api.L.*
import com.raquo.laminar.api.features.unitArrows import com.raquo.laminar.api.features.unitArrows
import rdts.base.Uid
import scala.util.Try import scala.util.Try
class EntryComponent( class EntryComponent(
entry: Entry, entry: Entry,
editMode: Boolean, editMode: Boolean,
editClickBus: EventBus[(Uid, Boolean)], editClickBus: EventBus[(EntryId, Boolean)],
entryEditBus: EventBus[Entry] entryEditBus: EventBus[Entry]
): ):
def render: ReactiveHtmlElement[HTMLTableRowElement] = { def render: ReactiveHtmlElement[HTMLTableRowElement] = {
@ -93,11 +92,12 @@ class EntryComponent(
editClickBus.emit(entry.id, false) editClickBus.emit(entry.id, false)
entryEditBus.emit( entryEditBus.emit(
entry.copy( entry.copy(
startKm = driver = entry.driver.write(driverInput.ref.value)
entry.startKm.write(startKmInput.ref.value.toDouble), // startKm =
endKm = entry.endKm.write(endKmInput.ref.value.toDouble), // entry.startKm.write(startKmInput.ref.value.toDouble),
animal = entry.animal.write(animalInput.ref.value), // endKm = entry.endKm.write(endKmInput.ref.value.toDouble),
paid = entry.paid.write(paidCheckbox.ref.checked) // animal = entry.animal.write(animalInput.ref.value),
// paid = entry.paid.write(paidCheckbox.ref.checked)
) )
) )
}, },

View file

@ -5,8 +5,7 @@ import com.raquo.laminar.api.L.*
import com.raquo.laminar.api.features.unitArrows import com.raquo.laminar.api.features.unitArrows
import fahrtenbuch.Main.entryEditBus import fahrtenbuch.Main.entryEditBus
import fahrtenbuch.model.Entry import fahrtenbuch.model.{Entry, EntryId}
import rdts.base.Uid
import rdts.datatypes.LastWriterWins import rdts.datatypes.LastWriterWins
import scala.scalajs.js.Date import scala.scalajs.js.Date
import scala.util.Try import scala.util.Try
@ -77,7 +76,7 @@ class NewEntryInput(showNewEntryField: Var[Boolean]):
button( button(
cls := "button is-success", cls := "button is-success",
onClick --> { onClick --> {
val id = Uid.gen() val id = EntryId.gen()
val driver = LastWriterWins.now(newEntryDriver.ref.value) val driver = LastWriterWins.now(newEntryDriver.ref.value)
val startKm = LastWriterWins.now(newEntryStartKm.ref.value.toDouble) val startKm = LastWriterWins.now(newEntryStartKm.ref.value.toDouble)
val endKm = LastWriterWins.now(newEntryEndKm.ref.value.toDouble) val endKm = LastWriterWins.now(newEntryEndKm.ref.value.toDouble)

View file

@ -0,0 +1,17 @@
package fahrtenbuch.components
import com.raquo.laminar.api.L.*
import com.raquo.airstream.core.Signal
class OnlineStatusComponent(online: Signal[Boolean]):
def render(): HtmlElement = {
val status = online.map {
case true => "Online"
case false => "Offline"
}
span(
cls := "tag",
text <-- status
)
}

View file

@ -6,9 +6,26 @@ import org.getshaka.nativeconverter.NativeConverter
import org.getshaka.nativeconverter.ParseState import org.getshaka.nativeconverter.ParseState
import scala.scalajs.js import scala.scalajs.js
import rdts.datatypes.LastWriterWins import rdts.datatypes.LastWriterWins
import rdts.base.Lattice
opaque type EntryId = Uid
object EntryId:
def gen(): EntryId = Uid.gen()
def apply(id: String): EntryId = Uid.predefined(id)
extension (id: EntryId) def delegate: String = id.delegate
given NativeConverter[EntryId] with {
extension (a: EntryId)
override def toNative: js.Any =
a.delegate
override def fromNative(ps: ParseState): Uid =
Uid.predefined(ps.json.asInstanceOf[String])
}
given Lattice[EntryId] = Lattice.assertEquals
case class Entry( case class Entry(
id: Uid, id: EntryId,
startKm: LastWriterWins[Double], startKm: LastWriterWins[Double],
endKm: LastWriterWins[Double], endKm: LastWriterWins[Double],
animal: LastWriterWins[String], animal: LastWriterWins[String],
@ -24,10 +41,4 @@ case class Entry(
def costTotal: Double = costGas + costWear def costTotal: Double = costGas + costWear
object Entry: object Entry:
given NativeConverter[Uid] with { given Lattice[Entry] = Lattice.derived
extension (a: Uid)
override def toNative: js.Any =
a.delegate
override def fromNative(ps: ParseState): Uid =
Uid.predefined(ps.json.asInstanceOf[String])
}