From 40ea76895e3212060f5f7fc2723b47747b07af63 Mon Sep 17 00:00:00 2001 From: Julian Date: Sat, 12 Jul 2025 14:02:54 +0200 Subject: [PATCH] WIP --- package.json | 2 +- src/main/scala/fahrtenbuch/Database.scala | 67 +++++++++++++++++-- src/main/scala/fahrtenbuch/Main.scala | 4 +- src/main/scala/fahrtenbuch/Networking.scala | 24 ++++++- src/main/scala/fahrtenbuch/Sync.scala | 7 ++ .../fahrtenbuch/components/AppComponent.scala | 16 +++-- .../components/EntryComponent.scala | 16 ++--- .../components/NewEntryInput.scala | 5 +- .../components/OnlineStatusComponent.scala | 17 +++++ src/main/scala/fahrtenbuch/model/Entry.scala | 27 +++++--- 10 files changed, 149 insertions(+), 36 deletions(-) create mode 100644 src/main/scala/fahrtenbuch/Sync.scala create mode 100644 src/main/scala/fahrtenbuch/components/OnlineStatusComponent.scala diff --git a/package.json b/package.json index e05e10b..9bfc11d 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", + "dev": "vite --host", "build": "vite build", "preview": "vite preview" }, diff --git a/src/main/scala/fahrtenbuch/Database.scala b/src/main/scala/fahrtenbuch/Database.scala index 95091c9..81961a0 100644 --- a/src/main/scala/fahrtenbuch/Database.scala +++ b/src/main/scala/fahrtenbuch/Database.scala @@ -12,10 +12,16 @@ import scala.concurrent.Future import scala.scalajs.js 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 { - private val schemaVersion = 1.0 + private val schemaVersion = 1.1 private val dexieDB: Dexie = new Dexie.^("fahrtenbuch") dexieDB @@ -31,9 +37,62 @@ object DexieDB { val entriesObservable: Observable[Future[Seq[Entry]]] = liveQuery(() => getAllEntries()) - def insertEntry(entry: Entry): Future[Any] = { - println(s"inserting Entry $entry") - entriesTable.put(entry.toNative).toFuture + def getEntry(id: EntryId): Future[Option[Entry]] = + entriesTable + .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]] = { diff --git a/src/main/scala/fahrtenbuch/Main.scala b/src/main/scala/fahrtenbuch/Main.scala index f74c61a..8a16503 100644 --- a/src/main/scala/fahrtenbuch/Main.scala +++ b/src/main/scala/fahrtenbuch/Main.scala @@ -13,11 +13,11 @@ import components.AppComponent @main def Fahrtenbuch(): Unit = - val appComponent = AppComponent(Main.allEntries) + val appComponent = AppComponent(Main.allEntries, Trystero.onlineStatus) renderOnDomContentLoaded( dom.document.getElementById("app"), - AppComponent(Main.allEntries).render() + appComponent.render() ) object Main { diff --git a/src/main/scala/fahrtenbuch/Networking.scala b/src/main/scala/fahrtenbuch/Networking.scala index 69c7018..250299c 100644 --- a/src/main/scala/fahrtenbuch/Networking.scala +++ b/src/main/scala/fahrtenbuch/Networking.scala @@ -1,6 +1,7 @@ package fahrtenbuch import com.raquo.laminar.api.L.* +import org.scalajs.dom import org.scalajs.dom.RTCConfiguration import org.scalajs.dom.RTCIceServer import org.scalajs.dom.RTCPeerConnection @@ -12,6 +13,10 @@ import typings.trystero.mod.joinRoom import typings.trystero.mod.selfId import scala.scalajs.js +import typings.trystero.mod.ActionProgress +import typings.trystero.mod.ActionSender +import typings.trystero.mod.ActionReceiver +import model.Entry object Trystero: private val eturn = new RTCIceServer: @@ -35,11 +40,13 @@ object Trystero: } // Public API - val room: Room = joinRoom(MyConfig, "fahrtenbuch") - val peerList: Var[List[(String, RTCPeerConnection)]] = Var(List.empty) + val roomId = dom.window.location.hash + val room: Room = joinRoom(MyConfig, roomId) + println(s"joining room $roomId") 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 = peerList.set(room.getPeers().toList) println(s"my peer ID is $selfId") @@ -51,3 +58,14 @@ object Trystero: println(s"$peerId left") 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) diff --git a/src/main/scala/fahrtenbuch/Sync.scala b/src/main/scala/fahrtenbuch/Sync.scala new file mode 100644 index 0000000..29f233b --- /dev/null +++ b/src/main/scala/fahrtenbuch/Sync.scala @@ -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(_)) diff --git a/src/main/scala/fahrtenbuch/components/AppComponent.scala b/src/main/scala/fahrtenbuch/components/AppComponent.scala index 5823a7a..ad32a10 100644 --- a/src/main/scala/fahrtenbuch/components/AppComponent.scala +++ b/src/main/scala/fahrtenbuch/components/AppComponent.scala @@ -1,17 +1,19 @@ package fahrtenbuch.components import com.raquo.laminar.api.L.* -import fahrtenbuch.model.Entry +import fahrtenbuch.model.{Entry, EntryId} 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 - val editClickBus = new EventBus[(Uid, Boolean)] + val editClickBus = new EventBus[(EntryId, Boolean)] // tracks which entries are currently being edited - val editStateSignal: Signal[Map[Uid, Boolean]] = - editClickBus.stream.foldLeft(Map.empty[Uid, Boolean]) { + val editStateSignal: Signal[Map[EntryId, Boolean]] = + editClickBus.stream.foldLeft(Map.empty[EntryId, Boolean]) { case (acc, (id, value)) => acc + (id -> value) } @@ -37,7 +39,7 @@ class AppComponent(allEntries: Signal[Set[Entry]]): def render(): HtmlElement = div( cls := "app content", - h1("Fahrtenbuch"), + h1("Fahrtenbuch", OnlineStatusComponent(onlineStatus).render()), table( cls := "table", thead( diff --git a/src/main/scala/fahrtenbuch/components/EntryComponent.scala b/src/main/scala/fahrtenbuch/components/EntryComponent.scala index 27dbbd2..8cc5421 100644 --- a/src/main/scala/fahrtenbuch/components/EntryComponent.scala +++ b/src/main/scala/fahrtenbuch/components/EntryComponent.scala @@ -1,16 +1,15 @@ package fahrtenbuch.components -import fahrtenbuch.model.Entry +import fahrtenbuch.model.{Entry, EntryId} import com.raquo.laminar.nodes.ReactiveHtmlElement import org.scalajs.dom.HTMLTableRowElement import com.raquo.laminar.api.L.* import com.raquo.laminar.api.features.unitArrows -import rdts.base.Uid import scala.util.Try class EntryComponent( entry: Entry, editMode: Boolean, - editClickBus: EventBus[(Uid, Boolean)], + editClickBus: EventBus[(EntryId, Boolean)], entryEditBus: EventBus[Entry] ): def render: ReactiveHtmlElement[HTMLTableRowElement] = { @@ -93,11 +92,12 @@ class EntryComponent( editClickBus.emit(entry.id, false) entryEditBus.emit( entry.copy( - startKm = - entry.startKm.write(startKmInput.ref.value.toDouble), - endKm = entry.endKm.write(endKmInput.ref.value.toDouble), - animal = entry.animal.write(animalInput.ref.value), - paid = entry.paid.write(paidCheckbox.ref.checked) + driver = entry.driver.write(driverInput.ref.value) +// startKm = +// entry.startKm.write(startKmInput.ref.value.toDouble), +// endKm = entry.endKm.write(endKmInput.ref.value.toDouble), +// animal = entry.animal.write(animalInput.ref.value), +// paid = entry.paid.write(paidCheckbox.ref.checked) ) ) }, diff --git a/src/main/scala/fahrtenbuch/components/NewEntryInput.scala b/src/main/scala/fahrtenbuch/components/NewEntryInput.scala index ccfd91c..352f8cd 100644 --- a/src/main/scala/fahrtenbuch/components/NewEntryInput.scala +++ b/src/main/scala/fahrtenbuch/components/NewEntryInput.scala @@ -5,8 +5,7 @@ import com.raquo.laminar.api.L.* import com.raquo.laminar.api.features.unitArrows import fahrtenbuch.Main.entryEditBus -import fahrtenbuch.model.Entry -import rdts.base.Uid +import fahrtenbuch.model.{Entry, EntryId} import rdts.datatypes.LastWriterWins import scala.scalajs.js.Date import scala.util.Try @@ -77,7 +76,7 @@ class NewEntryInput(showNewEntryField: Var[Boolean]): button( cls := "button is-success", onClick --> { - val id = Uid.gen() + val id = EntryId.gen() val driver = LastWriterWins.now(newEntryDriver.ref.value) val startKm = LastWriterWins.now(newEntryStartKm.ref.value.toDouble) val endKm = LastWriterWins.now(newEntryEndKm.ref.value.toDouble) diff --git a/src/main/scala/fahrtenbuch/components/OnlineStatusComponent.scala b/src/main/scala/fahrtenbuch/components/OnlineStatusComponent.scala new file mode 100644 index 0000000..b0c8280 --- /dev/null +++ b/src/main/scala/fahrtenbuch/components/OnlineStatusComponent.scala @@ -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 + ) + } diff --git a/src/main/scala/fahrtenbuch/model/Entry.scala b/src/main/scala/fahrtenbuch/model/Entry.scala index 499ef3e..4d620a1 100644 --- a/src/main/scala/fahrtenbuch/model/Entry.scala +++ b/src/main/scala/fahrtenbuch/model/Entry.scala @@ -6,9 +6,26 @@ import org.getshaka.nativeconverter.NativeConverter import org.getshaka.nativeconverter.ParseState import scala.scalajs.js 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( - id: Uid, + id: EntryId, startKm: LastWriterWins[Double], endKm: LastWriterWins[Double], animal: LastWriterWins[String], @@ -24,10 +41,4 @@ case class Entry( def costTotal: Double = costGas + costWear object Entry: - given NativeConverter[Uid] with { - extension (a: Uid) - override def toNative: js.Any = - a.delegate - override def fromNative(ps: ParseState): Uid = - Uid.predefined(ps.json.asInstanceOf[String]) - } + given Lattice[Entry] = Lattice.derived