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",
"type": "module",
"scripts": {
"dev": "vite",
"dev": "vite --host",
"build": "vite build",
"preview": "vite preview"
},

View file

@ -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")
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]] = {

View file

@ -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 {

View file

@ -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)

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
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(

View file

@ -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)
)
)
},

View file

@ -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)

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 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