Skip to content

Commit

Permalink
Add StreamingRender iterator to support streaming template renders
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaelHatherly committed Dec 6, 2024
1 parent 5c16387 commit 1004fe2
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 1 deletion.
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240"
MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
PackageExtensionCompat = "65ce6f38-6b18-4e1d-a461-8949797d7930"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
SimpleBufferStream = "777ac1f9-54b0-4bf8-805c-2214025038e7"
TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76"

[weakdeps]
Expand All @@ -28,5 +29,6 @@ CodeTracking = "1.3"
InteractiveUtils = "1.6"
MacroTools = "0.5"
PackageExtensionCompat = "1"
SimpleBufferStream = "1"
TOML = "1"
julia = "1.6"
20 changes: 19 additions & 1 deletion docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ _Hypertext templating DSL for Julia_
This package provides a collection of macros for building and rendering HTML
from Julia code using all the normal control flow syntax of the language, such
as loops and conditional. No intermediate "virtual" DOM is constructed during
rendering process, which reduces memory allocations.
rendering process, which reduces memory allocations. Supports streaming renders
of templates via a `StreamingRender` iterator.

When rendered in "development" mode source locations of all elements within the
rendered within the DOM are preserved, which allows for lookup from the browser
Expand Down Expand Up @@ -173,6 +174,23 @@ means that any keyword props provided in the component definition, such as
`prop` above can be interpolated into the markdown file and will be rendered
into the final HTML output that the component generates.

## `StreamingRender`

A `StreamingRender` is an iterator that handles asynchronous execution of
`@render` calls. This is useful if your `@component` potentially takes a long
time to render completely and you wish to begin streaming the HTML to the
browser as it becomes available.

```julia
for bytes in StreamingRender(io -> @render io @slow_component {args...})
write(http_stream, bytes)
end
```

`do`-block syntax is also, naturally, supported by the `StreamingRender`
constructor. All `@component` definitions support streaming out-of-the-box. Be
aware that rendering happens in a `Threads.@spawn`ed task.

## Docstrings

```@autodocs
Expand Down
3 changes: 3 additions & 0 deletions src/HypertextTemplates.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module HypertextTemplates
import CodeTracking
import PackageExtensionCompat
import MacroTools
import SimpleBufferStream

# Exports:

Expand All @@ -18,6 +19,7 @@ export @esc_str
export @render
export @text
export SafeString
export StreamingRender

# Includes:

Expand All @@ -36,6 +38,7 @@ include("deftag.jl")
include("Elements.jl")
include("template-source-lookup.jl")
include("render.jl")
include("stream.jl")
include("cmfile.jl")

# Initialization:
Expand Down
40 changes: 40 additions & 0 deletions src/stream.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""
StreamingRender(func)
An iterable that will run the render function `func`, which takes a single `io`
argument that must be passed to the `@render` macro call.
```julia
for bytes in StreamingRender(io -> @render io @component {args...})
write(http_stream, bytes)
end
```
Or use a `do` block rather than `->` syntax.
"""
struct StreamingRender
io::SimpleBufferStream.BufferStream
task::Task

function StreamingRender(f)
io = SimpleBufferStream.BufferStream()
task = Threads.@spawn begin
f(io)
close(io)
end
return new(io, task)
end
end

function Base.iterate(s::StreamingRender, state = nothing)
bytes = readavailable(s.io)
if isempty(bytes)
wait(s.task)
return nothing
else
return bytes, nothing
end
end
Base.eltype(::Type{StreamingRender}) = Vector{UInt8}
Base.IteratorSize(::Type{StreamingRender}) = Base.SizeUnknown()

21 changes: 21 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,17 @@ end
@cm_component markdown_component(; x) = joinpath(@__DIR__, "markdown.md")
@deftag macro markdown_component end

@component function streaming(; n::Integer)
@div {class = "streamed"} begin
@ul begin
for id = 1:n
@li {id} "This is item $id."
end
end
end
end
@deftag macro streaming end

@testset "HypertestTemplates" begin
@testset "Basics" begin
render_test("references/basics/html-elements.txt") do io
Expand Down Expand Up @@ -184,4 +195,14 @@ end
result = @render @p "content"
@test contains(result, "data-htloc=\"$file:$(line + 2)\"")
end
@testset "Streaming" begin
func(io = Vector{UInt8}) = @render io @streaming {n = 10000}
output = UInt8[]
for bytes in StreamingRender(func)
@assert !isempty(bytes)
append!(output, bytes)
end
@test length(output) > 1
@test output == func()
end
end

0 comments on commit 1004fe2

Please sign in to comment.