I just merged a PR in my HTML package elementary that I think is really cool - but I am a bit scared...
My goal was to find a way to support awaiting stuff inside an element's @resultBuilder
"body", as that would make for quite a nice server feature (ie: start streaming HTML down the pipe while some nested part of a page is still fetching).
I naively just added @escaping @Sendable ... async
to an existing result builder closure - honestly not expecting it to work at all - but it just did. I am having a hard time fully grasping what compiler wizardry is going on here, but things seem to magically just work.
On the forums I only found very few posts (and rather old ones) about this combination (some vaguely mentioning issues with this), so my question basically is:
How close to the edge am I driving here?
Are we in "totally fully supported no big deal", or rather in "don't look at it too fast or it'll fall apart" territory?
Does anyone have experience with this combination?
Here is a bit of code to show what I am scared of:
public struct AsyncContent<Content: HTML>: HTML, Sendable {
var content: @Sendable () async throws -> Content
public init(@HTMLBuilder content: @escaping @Sendable () async throws -> Content) {
self.content = content
}
@_spi(Rendering)
public static func _render<Renderer: _AsyncHTMLRendering>(_ html: consuming Self, into renderer: inout Renderer, with context: consuming _RenderingContext) async throws {
try await Content._render(await html.content(), into: &renderer, with: context)
}
}
// all HTML elements have an initializer overload that auto-wraps an async closure in this AsyncContent type
div {
let text = await getMyData()
p { "This totally works: \(text)" }
MyComponent()
}
struct MyComponent: HTML {
var content: some HTML {
AsyncContent {
"So does this: \(await getMoreData())"
}
}
}