mirror of
https://github.com/phfroidmont/scalive.git
synced 2025-12-25 13:36:59 +01:00
Support click events
This commit is contained in:
parent
1bd65fd49c
commit
124239925d
16 changed files with 277 additions and 162 deletions
|
|
@ -6,13 +6,13 @@ final case class MyModel(
|
|||
elems: List[Elem] = List.empty)
|
||||
final case class Elem(name: String, age: Int)
|
||||
|
||||
class TestView(initialModel: MyModel) extends LiveView[TestView.Cmd]:
|
||||
import TestView.Cmd.*
|
||||
class TestView(initialModel: MyModel) extends LiveView[String, TestView.Event]:
|
||||
import TestView.Event.*
|
||||
|
||||
private val modelVar = Var[MyModel](initialModel)
|
||||
|
||||
def handleCommand(cmd: TestView.Cmd): Unit =
|
||||
cmd match
|
||||
override def handleServerEvent(e: TestView.Event): Unit =
|
||||
e match
|
||||
case UpdateModel(f) => modelVar.update(f)
|
||||
|
||||
val el: HtmlElement =
|
||||
|
|
@ -33,5 +33,5 @@ class TestView(initialModel: MyModel) extends LiveView[TestView.Cmd]:
|
|||
)
|
||||
|
||||
object TestView:
|
||||
enum Cmd:
|
||||
enum Event:
|
||||
case UpdateModel(f: MyModel => MyModel)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import scalive.*
|
||||
import zio.json.JsonCodec
|
||||
|
||||
@main
|
||||
def main =
|
||||
|
|
@ -9,21 +10,21 @@ def main =
|
|||
Elem("c", 30)
|
||||
)
|
||||
)
|
||||
val s = Socket(TestView(initModel))
|
||||
val s = Socket("", "", TestView(initModel))
|
||||
println("Init")
|
||||
println(s.renderHtml())
|
||||
s.syncClient
|
||||
s.syncClient
|
||||
|
||||
println("Edit class attribue")
|
||||
s.receiveCommand(
|
||||
TestView.Cmd.UpdateModel(_.copy(cls = "text-lg"))
|
||||
s.lv.handleServerEvent(
|
||||
TestView.Event.UpdateModel(_.copy(cls = "text-lg"))
|
||||
)
|
||||
s.syncClient
|
||||
|
||||
println("Edit first and last")
|
||||
s.receiveCommand(
|
||||
TestView.Cmd.UpdateModel(
|
||||
s.lv.handleServerEvent(
|
||||
TestView.Event.UpdateModel(
|
||||
_.copy(elems =
|
||||
List(
|
||||
Elem("x", 10),
|
||||
|
|
@ -37,8 +38,8 @@ def main =
|
|||
println(s.renderHtml())
|
||||
|
||||
println("Add one")
|
||||
s.receiveCommand(
|
||||
TestView.Cmd.UpdateModel(
|
||||
s.lv.handleServerEvent(
|
||||
TestView.Event.UpdateModel(
|
||||
_.copy(elems =
|
||||
List(
|
||||
Elem("x", 10),
|
||||
|
|
@ -53,8 +54,8 @@ def main =
|
|||
println(s.renderHtml())
|
||||
|
||||
println("Remove first")
|
||||
s.receiveCommand(
|
||||
TestView.Cmd.UpdateModel(
|
||||
s.lv.handleServerEvent(
|
||||
TestView.Event.UpdateModel(
|
||||
_.copy(elems =
|
||||
List(
|
||||
Elem("b", 15),
|
||||
|
|
@ -68,8 +69,8 @@ def main =
|
|||
println(s.renderHtml())
|
||||
|
||||
println("Remove all")
|
||||
s.receiveCommand(
|
||||
TestView.Cmd.UpdateModel(
|
||||
s.lv.handleServerEvent(
|
||||
TestView.Event.UpdateModel(
|
||||
_.copy(
|
||||
cls = "text-lg",
|
||||
bool = false,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ enum Diff:
|
|||
dynamic: Seq[Diff.Dynamic] = Seq.empty)
|
||||
case Comprehension(
|
||||
static: Seq[String] = Seq.empty,
|
||||
entries: Seq[Diff.Dynamic] = Seq.empty)
|
||||
entries: Seq[Diff.Dynamic] = Seq.empty,
|
||||
count: Int = 0)
|
||||
case Value(value: String)
|
||||
case Dynamic(key: String, diff: Diff)
|
||||
case Deleted
|
||||
|
|
@ -29,19 +30,17 @@ object Diff:
|
|||
dynamic.map(d => d.key -> toJson(d.diff))
|
||||
)
|
||||
)
|
||||
case Diff.Comprehension(static, entries) =>
|
||||
case Diff.Comprehension(static, entries, count) =>
|
||||
Json.Obj(
|
||||
Option
|
||||
.when(static.nonEmpty)("s" -> Json.Arr(static.map(Json.Str(_))*))
|
||||
.to(Chunk)
|
||||
.appendedAll(
|
||||
Option.when(entries.nonEmpty)(
|
||||
"k" ->
|
||||
Json
|
||||
.Obj(
|
||||
entries.map(d => d.key -> toJson(d.diff))*
|
||||
).add("kc", Json.Num(entries.length))
|
||||
)
|
||||
.appended(
|
||||
"k" ->
|
||||
Json
|
||||
.Obj(
|
||||
entries.map(d => d.key -> toJson(d.diff))*
|
||||
).add("kc", Json.Num(count))
|
||||
)
|
||||
)
|
||||
case Diff.Value(value) => Json.Str(value)
|
||||
|
|
|
|||
|
|
@ -23,10 +23,10 @@ object DiffBuilder:
|
|||
|
||||
private def buildDynamic(dynamicMods: Seq[DynamicMod], trackUpdates: Boolean): Seq[Option[Diff]] =
|
||||
dynamicMods.flatMap {
|
||||
case Attr.Dyn(attr, value) =>
|
||||
case Attr.Dyn(name, value, _) =>
|
||||
List(value.render(trackUpdates).map(v => Diff.Value(v.toString)))
|
||||
case Attr.DynValueAsPresence(attr, value) =>
|
||||
List(value.render(trackUpdates).map(v => Diff.Value(if v then s" ${attr.name}" else "")))
|
||||
case Attr.DynValueAsPresence(name, value) =>
|
||||
List(value.render(trackUpdates).map(v => Diff.Value(if v then s" $name" else "")))
|
||||
case Content.Tag(el) => buildDynamic(el.dynamicMods, trackUpdates)
|
||||
case Content.DynText(dyn) => List(dyn.render(trackUpdates).map(Diff.Value(_)))
|
||||
case Content.DynElement(dyn) => ???
|
||||
|
|
@ -40,23 +40,25 @@ object DiffBuilder:
|
|||
case None => dyn.currentValue.map(build(_, trackUpdates)))
|
||||
case Content.DynElementColl(dyn) => ???
|
||||
case Content.DynSplit(splitVar) =>
|
||||
val entries = splitVar.render(trackUpdates)
|
||||
if entries.isEmpty then List.empty
|
||||
else
|
||||
val static =
|
||||
entries.collectFirst { case (_, Some(el)) => el.static }.getOrElse(List.empty)
|
||||
List(
|
||||
Some(
|
||||
Diff.Comprehension(
|
||||
static = if trackUpdates then Seq.empty else static,
|
||||
entries = entries.map {
|
||||
case (key, Some(el)) =>
|
||||
Diff.Dynamic(key.toString, build(Seq.empty, el.dynamicMods, trackUpdates))
|
||||
case (key, None) => Diff.Dynamic(key.toString, Diff.Deleted)
|
||||
}
|
||||
splitVar.render(trackUpdates) match
|
||||
case Some((entries, keysCount)) =>
|
||||
val static =
|
||||
entries.collectFirst { case (_, Some(el)) => el.static }.getOrElse(List.empty)
|
||||
List(
|
||||
Some(
|
||||
Diff.Comprehension(
|
||||
static = if trackUpdates then Seq.empty else static,
|
||||
entries = entries.map {
|
||||
case (key, Some(el)) =>
|
||||
Diff.Dynamic(key.toString, build(Seq.empty, el.dynamicMods, trackUpdates))
|
||||
case (key, None) => Diff.Dynamic(key.toString, Diff.Deleted)
|
||||
},
|
||||
count = keysCount
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
case None => List(None)
|
||||
|
||||
}
|
||||
|
||||
end DiffBuilder
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ class SplitVar[I, O, Key](
|
|||
project: (Key, Dyn[I]) => O):
|
||||
|
||||
// Deleted elements have value none
|
||||
private val memoized: mutable.Map[Key, Option[(Var[I], O)]] =
|
||||
private val memoized: mutable.Map[Key, (Var[I], O)] =
|
||||
mutable.Map.empty
|
||||
|
||||
private[scalive] def sync(): Unit =
|
||||
|
|
@ -107,30 +107,38 @@ class SplitVar[I, O, Key](
|
|||
nextKeys += entryKey
|
||||
memoized.updateWith(entryKey) {
|
||||
// Update matching key
|
||||
case varAndOutput @ Some(Some((entryVar, _))) =>
|
||||
case varAndOutput @ Some((entryVar, _)) =>
|
||||
entryVar.set(input)
|
||||
varAndOutput
|
||||
// Create new item
|
||||
case Some(None) | None =>
|
||||
case None =>
|
||||
val newVar = Var(input)
|
||||
Some(Some(newVar, project(entryKey, newVar)))
|
||||
Some(newVar, project(entryKey, newVar))
|
||||
}
|
||||
)
|
||||
memoized.keys.foreach(k => if !nextKeys.contains(k) then memoized.update(k, None))
|
||||
memoized.keys.foreach(k =>
|
||||
if !nextKeys.contains(k) then
|
||||
val _ = memoized.remove(k)
|
||||
)
|
||||
|
||||
private[scalive] def render(trackUpdates: Boolean): List[(Key, Option[O])] =
|
||||
memoized.collect {
|
||||
case (k, Some(entryVar, output)) if !trackUpdates || entryVar.changed => (k, Some(output))
|
||||
case (k, None) => (k, None)
|
||||
}.toList
|
||||
private[scalive] def render(trackUpdates: Boolean)
|
||||
: Option[(changeList: List[(Key, Option[O])], keysCount: Int)] =
|
||||
if parent.changed || !trackUpdates then
|
||||
Some(
|
||||
(
|
||||
memoized.collect {
|
||||
case (k, (entryVar, output)) if !trackUpdates || entryVar.changed => (k, Some(output))
|
||||
}.toList,
|
||||
memoized.size
|
||||
)
|
||||
)
|
||||
else None
|
||||
|
||||
private[scalive] def setUnchanged(): Unit =
|
||||
parent.setUnchanged()
|
||||
// Remove previously deleted
|
||||
memoized.filterInPlace((_, v) => v.nonEmpty)
|
||||
|
||||
// Usefull to call setUnchanged when the output is an HtmlElement as only the caller can know the type
|
||||
private[scalive] def callOnEveryChild(f: O => Unit): Unit =
|
||||
memoized.values.foreach(_.foreach((_, output) => f(output)))
|
||||
memoized.values.foreach((_, output) => f(output))
|
||||
|
||||
end SplitVar
|
||||
|
|
|
|||
|
|
@ -20,11 +20,11 @@ object HtmlBuilder:
|
|||
for i <- dynamic.indices do
|
||||
strw.write(static(i))
|
||||
dynamic(i) match
|
||||
case Attr.Dyn(attr, value) =>
|
||||
strw.write(value.render(false).map(attr.codec.encode).getOrElse(""))
|
||||
case Attr.DynValueAsPresence(attr, value) =>
|
||||
case Attr.Dyn(name, value, isJson) =>
|
||||
strw.write(value.render(false).getOrElse(""))
|
||||
case Attr.DynValueAsPresence(name, value) =>
|
||||
strw.write(
|
||||
value.render(false).map(if _ then s" ${attr.name}" else "").getOrElse("")
|
||||
value.render(false).map(if _ then s" $name" else "").getOrElse("")
|
||||
)
|
||||
case Content.Tag(el) => build(el.static, el.dynamicMods, strw)
|
||||
case Content.DynText(dyn) => strw.write(dyn.render(false).getOrElse(""))
|
||||
|
|
@ -33,8 +33,8 @@ object HtmlBuilder:
|
|||
dyn.render(false).foreach(_.foreach(el => build(el.static, el.dynamicMods, strw)))
|
||||
case Content.DynElementColl(dyn) => ???
|
||||
case Content.DynSplit(splitVar) =>
|
||||
val entries = splitVar.render(false)
|
||||
val staticOpt = entries.collectFirst { case (_, Some(el)) => el.static }
|
||||
val (entries, _) = splitVar.render(false).getOrElse(List.empty -> 0)
|
||||
val staticOpt = entries.collectFirst { case (_, Some(el)) => el.static }
|
||||
entries.foreach {
|
||||
case (_, Some(entryEl)) =>
|
||||
build(staticOpt.getOrElse(Nil), entryEl.dynamicMods, strw)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
package scalive
|
||||
|
||||
import scalive.codecs.BooleanAsAttrPresenceCodec
|
||||
import scalive.codecs.Codec
|
||||
import scalive.Mod.Attr
|
||||
import scalive.Mod.Content
|
||||
import scalive.codecs.BooleanAsAttrPresenceCodec
|
||||
import scalive.codecs.Codec
|
||||
import zio.json.*
|
||||
|
||||
class HtmlElement(val tag: HtmlTag, val mods: Vector[Mod]):
|
||||
def static: Seq[String] = StaticBuilder.build(this)
|
||||
|
|
@ -36,18 +37,26 @@ class HtmlAttr[V](val name: String, val codec: Codec[V, String]):
|
|||
def :=(value: V): Mod.Attr =
|
||||
if isBooleanAsAttrPresence then
|
||||
Mod.Attr.StaticValueAsPresence(
|
||||
this.asInstanceOf[HtmlAttr[Boolean]],
|
||||
name,
|
||||
value.asInstanceOf[Boolean]
|
||||
)
|
||||
else Mod.Attr.Static(this, codec.encode(value))
|
||||
else Mod.Attr.Static(name, codec.encode(value))
|
||||
|
||||
def :=(value: Dyn[V]): Mod.Attr =
|
||||
if isBooleanAsAttrPresence then
|
||||
Mod.Attr.DynValueAsPresence(
|
||||
this.asInstanceOf[HtmlAttr[Boolean]],
|
||||
name,
|
||||
value.asInstanceOf[Dyn[Boolean]]
|
||||
)
|
||||
else Mod.Attr.Dyn(this, value)
|
||||
else Mod.Attr.Dyn(name, value(codec.encode))
|
||||
|
||||
class HtmlAttrJsonValue(val name: String):
|
||||
|
||||
def :=[V: JsonCodec](value: V): Mod.Attr =
|
||||
Mod.Attr.Static(name, value.toJson, isJson = true)
|
||||
|
||||
def :=[V: JsonCodec](value: Dyn[V]): Mod.Attr =
|
||||
Mod.Attr.Dyn(name, value(_.toJson), isJson = true)
|
||||
|
||||
sealed trait Mod
|
||||
sealed trait StaticMod extends Mod
|
||||
|
|
@ -55,12 +64,12 @@ sealed trait DynamicMod extends Mod
|
|||
|
||||
object Mod:
|
||||
enum Attr extends Mod:
|
||||
case Static(attr: HtmlAttr[?], value: String) extends Attr with StaticMod
|
||||
case StaticValueAsPresence(attr: HtmlAttr[Boolean], value: Boolean) extends Attr with StaticMod
|
||||
case Dyn[T](attr: HtmlAttr[T], value: scalive.Dyn[T]) extends Attr with DynamicMod
|
||||
case DynValueAsPresence(attr: HtmlAttr[Boolean], value: scalive.Dyn[Boolean])
|
||||
case Static(name: String, value: String, isJson: Boolean = false) extends Attr with StaticMod
|
||||
case StaticValueAsPresence(name: String, value: Boolean) extends Attr with StaticMod
|
||||
case Dyn(name: String, value: scalive.Dyn[String], isJson: Boolean = false)
|
||||
extends Attr
|
||||
with DynamicMod
|
||||
case DynValueAsPresence(name: String, value: scalive.Dyn[Boolean]) extends Attr with DynamicMod
|
||||
|
||||
enum Content extends Mod:
|
||||
case Text(text: String) extends Content with StaticMod
|
||||
|
|
@ -75,14 +84,14 @@ object Mod:
|
|||
extension (mod: Mod)
|
||||
private[scalive] def setAllUnchanged(): Unit =
|
||||
mod match
|
||||
case Attr.Static(_, _) => ()
|
||||
case Attr.StaticValueAsPresence(_, _) => ()
|
||||
case Attr.Dyn(_, value) => value.setUnchanged()
|
||||
case Attr.DynValueAsPresence(attr, value) => value.setUnchanged()
|
||||
case Content.Text(text) => ()
|
||||
case Content.Tag(el) => el.setAllUnchanged()
|
||||
case Content.DynText(dyn) => dyn.setUnchanged()
|
||||
case Content.DynElement(dyn) =>
|
||||
case Attr.Static(_, _, _) => ()
|
||||
case Attr.StaticValueAsPresence(_, _) => ()
|
||||
case Attr.Dyn(_, value, _) => value.setUnchanged()
|
||||
case Attr.DynValueAsPresence(_, value) => value.setUnchanged()
|
||||
case Content.Text(text) => ()
|
||||
case Content.Tag(el) => el.setAllUnchanged()
|
||||
case Content.DynText(dyn) => dyn.setUnchanged()
|
||||
case Content.DynElement(dyn) =>
|
||||
dyn.setUnchanged()
|
||||
dyn.callOnEveryChild(_.setAllUnchanged())
|
||||
case Content.DynOptionElement(dyn) =>
|
||||
|
|
@ -97,14 +106,14 @@ extension (mod: Mod)
|
|||
|
||||
private[scalive] def syncAll(): Unit =
|
||||
mod match
|
||||
case Attr.Static(_, _) => ()
|
||||
case Attr.StaticValueAsPresence(_, _) => ()
|
||||
case Attr.Dyn(_, value) => value.sync()
|
||||
case Attr.DynValueAsPresence(attr, value) => value.sync()
|
||||
case Content.Text(text) => ()
|
||||
case Content.Tag(el) => el.syncAll()
|
||||
case Content.DynText(dyn) => dyn.sync()
|
||||
case Content.DynElement(dyn) =>
|
||||
case Attr.Static(_, _, _) => ()
|
||||
case Attr.StaticValueAsPresence(_, _) => ()
|
||||
case Attr.Dyn(_, value, _) => value.sync()
|
||||
case Attr.DynValueAsPresence(_, value) => value.sync()
|
||||
case Content.Text(text) => ()
|
||||
case Content.Tag(el) => el.syncAll()
|
||||
case Content.DynText(dyn) => dyn.sync()
|
||||
case Content.DynElement(dyn) =>
|
||||
dyn.sync()
|
||||
dyn.callOnEveryChild(_.syncAll())
|
||||
case Content.DynOptionElement(dyn) =>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package scalive
|
||||
|
||||
trait LiveView[Cmd]:
|
||||
def handleCommand(cmd: Cmd): Unit
|
||||
trait LiveView[ClientEvt, ServerEvent]:
|
||||
def handleClientEvent(evt: ClientEvt): Unit = ()
|
||||
def handleServerEvent(evt: ServerEvent): Unit = ()
|
||||
val el: HtmlElement
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import scalive.codecs.StringAsIsCodec
|
||||
import scalive.defs.attrs.HtmlAttrs
|
||||
import scalive.defs.complex.ComplexHtmlKeys
|
||||
import scalive.defs.tags.HtmlTags
|
||||
|
|
@ -6,6 +7,18 @@ package object scalive extends HtmlTags with HtmlAttrs with ComplexHtmlKeys:
|
|||
|
||||
lazy val defer = htmlAttr("defer", codecs.BooleanAsOnOffStringCodec)
|
||||
|
||||
object phx:
|
||||
private def phxAttr(suffix: String): HtmlAttr[String] =
|
||||
new HtmlAttr(s"phx-$suffix", StringAsIsCodec)
|
||||
private def phxAttrJson(suffix: String): HtmlAttrJsonValue =
|
||||
new HtmlAttrJsonValue(s"phx-$suffix")
|
||||
private def dataPhxAttr(suffix: String): HtmlAttr[String] =
|
||||
new HtmlAttr(s"data-phx-$suffix", StringAsIsCodec)
|
||||
|
||||
private[scalive] lazy val session = dataPhxAttr("session")
|
||||
lazy val click = phxAttrJson("click")
|
||||
def value(key: String) = phxAttr(s"value-$key")
|
||||
|
||||
implicit def stringToMod(v: String): Mod = Mod.Content.Text(v)
|
||||
implicit def htmlElementToMod(el: HtmlElement): Mod = Mod.Content.Tag(el)
|
||||
implicit def dynStringToMod(d: Dyn[String]): Mod = Mod.Content.DynText(d)
|
||||
|
|
|
|||
|
|
@ -2,28 +2,23 @@ package scalive
|
|||
|
||||
import zio.json.*
|
||||
|
||||
import java.util.Base64
|
||||
import scala.util.Random
|
||||
|
||||
final case class Socket[Cmd](lv: LiveView[Cmd]):
|
||||
final case class Socket[CliEvt: JsonCodec, SrvEvt](
|
||||
id: String,
|
||||
token: String,
|
||||
lv: LiveView[CliEvt, SrvEvt]):
|
||||
val clientEventCodec = JsonCodec[CliEvt]
|
||||
|
||||
private var clientInitialized = false
|
||||
val id: String =
|
||||
s"phx-${Base64.getUrlEncoder().withoutPadding().encodeToString(Random().nextBytes(8))}"
|
||||
private val token = Token.sign("secret", id, "")
|
||||
|
||||
lv.el.syncAll()
|
||||
|
||||
def receiveCommand(cmd: Cmd): Unit =
|
||||
lv.handleCommand(cmd)
|
||||
|
||||
def renderHtml(rootLayout: HtmlElement => HtmlElement = identity): String =
|
||||
lv.el.syncAll()
|
||||
HtmlBuilder.build(
|
||||
rootLayout(
|
||||
div(
|
||||
idAttr := id,
|
||||
dataAttr("phx-session") := token,
|
||||
idAttr := id,
|
||||
phx.session := token,
|
||||
lv.el
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,10 +12,14 @@ object StaticBuilder:
|
|||
|
||||
private def buildStaticFragments(el: HtmlElement): Seq[Option[String]] =
|
||||
val attrs = el.attrMods.flatMap {
|
||||
case Attr.Static(attr, value) => List(Some(s""" ${attr.name}="$value""""))
|
||||
case Attr.StaticValueAsPresence(attr, value) => List(Some(s" ${attr.name}"))
|
||||
case Attr.Dyn(attr, value) => List(Some(s""" ${attr.name}=""""), None, Some('"'.toString))
|
||||
case Attr.DynValueAsPresence(attr, value) => List(Some(""), None, Some(""))
|
||||
case Attr.Static(name, value, isJson) =>
|
||||
if isJson then List(Some(s" $name='$value'"))
|
||||
else List(Some(s""" $name="$value""""))
|
||||
case Attr.StaticValueAsPresence(name, value) => List(Some(s" $name"))
|
||||
case Attr.Dyn(name, value, isJson) =>
|
||||
if isJson then List(Some(s" $name='"), None, Some("'"))
|
||||
else List(Some(s""" $name=""""), None, Some('"'.toString))
|
||||
case Attr.DynValueAsPresence(_, value) => List(Some(""), None, Some(""))
|
||||
}
|
||||
val children = el.contentMods.flatMap {
|
||||
case Content.Text(text) => List(Some(text))
|
||||
|
|
|
|||
|
|
@ -35,10 +35,10 @@ object Token:
|
|||
mac.doFinal(value)
|
||||
|
||||
private def base64Encode(value: Array[Byte]): String =
|
||||
Base64.getEncoder().encodeToString(value)
|
||||
Base64.getUrlEncoder().withoutPadding().encodeToString(value)
|
||||
|
||||
private def base64Decode(value: String): Array[Byte] =
|
||||
Base64.getDecoder().decode(value)
|
||||
Base64.getUrlDecoder().decode(value)
|
||||
|
||||
def verify[T: JsonCodec](secret: String, token: String, maxAge: Duration)
|
||||
: Either[String, (liveViewId: String, payload: T)] =
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue