sake92 / hepek

Typesafe HTML templates and static site generator in pure Scala
https://sake92.github.io/hepek/
Apache License 2.0
104 stars 10 forks source link

Support for ZStream (or any other stream library) #268

Open carlos-verdes opened 6 months ago

carlos-verdes commented 6 months ago

I created an abstraction on top of scala.xml to serve content using ZIO.ZStream, so I can stream results from server instead of loading all content in memory and then flush to the client.

It would be really good to have a feature like that on your library.

sake92 commented 6 months ago

Hi @carlos-verdes !

Could you explain a bit more what you are requesting here?
An example maybe?

Hepek is using https://com-lihaoyi.github.io/scalatags undercover, which renders Frags as a String, in memory.

carlos-verdes commented 6 months ago

This is more or less what I have done with scala.xml

// my query returns a stream of Pet
val myPets: ZStream[R, Throwable, Pet] = ???
def petToNode(pet: Pet): scala.xml.Node = ???

// new type of document Node called StreamNode
case class StreamNode(nodes: Stream[Throwable, Node]) extends Node:
  def child: collection.Seq[xml.Node] = Seq.empty
  def label: String = "Stream"

// Inject the pet stream inside a dom tree 
val myTemplate: scala.xml.Node =
  <table>
    {{StreamNode(myPets.map(petToNode))}}
  </table>

// traverse the dom tree and create 1 stream entry per element
// when we reach our custom StreamNode we just need to map to string as it's already a stream
tree
extension (node: scala.xml.Node)
  def toZStream: Stream[Throwable, String] =
    node match
      case elem: Elem =>
        val attributes = if elem.attributes != null then elem.attributes.toString else ""
        val pre: Stream[Throwable, String] = ZStream("<" + elem.label + attributes + ">")
        elem.child.foldLeft(pre)((accum, node) => accum ++ node.toZStream) ++
            ZStream("</" + elem.label + ">")
      case Group(nodes) =>
        val emptyStream: Stream[Throwable, String] = ZStream.empty
        nodes.foldLeft(emptyStream)((accum, newNode) => accum ++ newNode.toZStream)
      case StreamNode(nodes) =>
        nodes.map(_.toString)
      case other => ZStream(other.toString)

// then in my app I can build a body from stream and serve streamed results (protecting my backedn from OOM errors)
def app = Http.collectHttp[Request]:

    // sources
    case Method.GET -> Root / "myPets" =>
      Handler
      .fromZIO:
        for env <- ZIO.environment[R]
        yield
          Handler.fromBody(Body.fromStream(myTemplate.toZStream))
      .flatten
      .toHttp 

Previous code will generate the next stream: ZStream("<table>") ++ myPetsStream.map(_.toString) ++ ZStream("</table>")

carlos-verdes commented 6 months ago

I'm opening these issues as suggestions but also I'll try to participate with my own code