Skip to content

Commit 4d6affc

Browse files
HTMLResultRepresentation.streamAsync now accepts a closure instead of a Duration
1 parent ef17250 commit 4d6affc

File tree

7 files changed

+126
-89
lines changed

7 files changed

+126
-89
lines changed

Sources/HTMLKitParse/ExpandHTMLMacro.swift

Lines changed: 38 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ extension HTMLKitUtilities {
8080
static func representationResult(
8181
encoding: HTMLEncoding,
8282
encodedResult: String,
83-
representation: HTMLResultRepresentation
83+
representation: HTMLResultRepresentationAST
8484
) -> String {
8585
switch representation {
8686
case .literal:
@@ -102,9 +102,25 @@ extension HTMLKitUtilities {
102102
return "InlineArray<\(chunks.count), \(typeAnnotation)>([\(chunks)])"
103103
#endif
104104
case .streamed(let optimized, let chunkSize):
105-
return streamed(encoding: encoding, encodedResult: encodedResult, async: false, optimized: optimized, chunkSize: chunkSize, suspendDuration: nil)
106-
case .streamedAsync(let optimized, let chunkSize, let suspendDuration):
107-
return streamed(encoding: encoding, encodedResult: encodedResult, async: true, optimized: optimized, chunkSize: chunkSize, suspendDuration: suspendDuration)
105+
return streamed(
106+
encoding: encoding,
107+
encodedResult: encodedResult,
108+
async: false,
109+
optimized: optimized,
110+
chunkSize: chunkSize,
111+
yieldVariableName: nil,
112+
afterYield: nil
113+
)
114+
case .streamedAsync(let optimized, let chunkSize, let yieldVariableName, let afterYield):
115+
return streamed(
116+
encoding: encoding,
117+
encodedResult: encodedResult,
118+
async: true,
119+
optimized: optimized,
120+
chunkSize: chunkSize,
121+
yieldVariableName: yieldVariableName,
122+
afterYield: afterYield
123+
)
108124
default:
109125
break
110126
}
@@ -232,23 +248,34 @@ extension HTMLKitUtilities {
232248
async: Bool,
233249
optimized: Bool,
234250
chunkSize: Int,
235-
suspendDuration: Duration?
251+
yieldVariableName: String?,
252+
afterYield: String?
236253
) -> String {
237254
var string = "AsyncStream { continuation in\n"
238255
if async {
239256
string += "Task {\n"
240257
}
241-
let duration:String?
242-
if let suspendDuration {
243-
duration = durationDebugDescription(suspendDuration)
258+
var yieldVariableName:String? = yieldVariableName
259+
if yieldVariableName == "_" {
260+
yieldVariableName = nil
261+
}
262+
var afterYieldLogic:String?
263+
if let afterYield {
264+
if let yieldVariableName {
265+
string += "var \(yieldVariableName) = 0\n"
266+
}
267+
afterYieldLogic = afterYield
244268
} else {
245-
duration = nil
269+
afterYieldLogic = nil
246270
}
247271
let chunks = chunks(encoding: encoding, encodedResult: encodedResult, async: async, optimized: optimized, chunkSize: chunkSize)
248272
for chunk in chunks {
249273
string += "continuation.yield(" + chunk + ")\n"
250-
if let duration {
251-
string += "try await Task.sleep(for: \(duration))\n"
274+
if let afterYieldLogic {
275+
string += "\(afterYieldLogic)\n"
276+
}
277+
if let yieldVariableName {
278+
string += "\(yieldVariableName) += 1\n"
252279
}
253280
}
254281
string += "continuation.finish()\n}"
@@ -257,29 +284,4 @@ extension HTMLKitUtilities {
257284
}
258285
return string
259286
}
260-
static func durationDebugDescription(_ duration: Duration) -> String {
261-
let (seconds, attoseconds) = duration.components
262-
var string:String
263-
if attoseconds == 0 {
264-
string = ".seconds(\(seconds))"
265-
} else {
266-
var nanoseconds = attoseconds / 1_000_000_000
267-
nanoseconds += seconds * 1_000_000_000
268-
string = "\(nanoseconds)"
269-
if seconds == 0 {
270-
if string.hasSuffix("000000") {
271-
string.removeLast(6)
272-
string = ".milliseconds(\(string))"
273-
} else if string.hasSuffix("000") {
274-
string.removeLast(3)
275-
string = ".microseconds(\(string))"
276-
} else {
277-
string = ".nanoseconds(\(string))"
278-
}
279-
} else {
280-
string = ".nanoseconds(\(nanoseconds))"
281-
}
282-
}
283-
return string
284-
}
285287
}

Sources/HTMLKitParse/ParseData.swift

Lines changed: 15 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ extension HTMLKitUtilities {
9999
}
100100

101101
// MARK: Parse Representation
102-
public static func parseRepresentation(expr: ExprSyntax) -> HTMLResultRepresentation? {
102+
public static func parseRepresentation(expr: ExprSyntax) -> HTMLResultRepresentationAST? {
103103
switch expr.kind {
104104
case .memberAccessExpr:
105105
switch expr.memberAccess!.declName.baseName.text {
@@ -117,7 +117,8 @@ extension HTMLKitUtilities {
117117
let function = expr.functionCall!
118118
var optimized = true
119119
var chunkSize = 1024
120-
var suspendDuration:Duration? = nil
120+
var yieldVariableName:String? = nil
121+
var afterYield:String? = nil
121122
for arg in function.arguments {
122123
switch arg.label?.text {
123124
case "optimized":
@@ -126,45 +127,19 @@ extension HTMLKitUtilities {
126127
if let s = arg.expression.integerLiteral?.literal.text, let size = Int(s) {
127128
chunkSize = size
128129
}
129-
case "suspendDuration":
130-
guard let function = arg.expression.functionCall else { break }
131-
var intValue:UInt64? = nil
132-
var doubleValue:Double? = nil
133-
if let v = function.arguments.first?.expression.integerLiteral?.literal.text, let i = UInt64(v) {
134-
intValue = i
135-
} else if let v = function.arguments.first?.expression.as(FloatLiteralExprSyntax.self)?.literal.text, let d = Double(v) {
136-
doubleValue = d
137-
} else {
138-
break
139-
}
140-
switch function.calledExpression.memberAccess?.declName.baseName.text {
141-
case "milliseconds":
142-
if let intValue {
143-
suspendDuration = .milliseconds(intValue)
144-
} else if let doubleValue {
145-
suspendDuration = .milliseconds(doubleValue)
146-
}
147-
case "microseconds":
148-
if let intValue {
149-
suspendDuration = .microseconds(intValue)
150-
} else if let doubleValue {
151-
suspendDuration = .microseconds(doubleValue)
152-
}
153-
case "nanoseconds":
154-
if let intValue {
155-
suspendDuration = .nanoseconds(intValue)
130+
default: // afterYield
131+
guard let closure = arg.expression.as(ClosureExprSyntax.self) else { break }
132+
if let parameters = closure.signature?.parameterClause {
133+
switch parameters {
134+
case .simpleInput(let shorthand):
135+
yieldVariableName = shorthand.first?.name.text
136+
case .parameterClause(let parameterSyntax):
137+
if let parameter = parameterSyntax.parameters.first {
138+
yieldVariableName = (parameter.secondName ?? parameter.firstName).text
139+
}
156140
}
157-
case "seconds":
158-
if let intValue {
159-
suspendDuration = .seconds(intValue)
160-
} else if let doubleValue {
161-
suspendDuration = .seconds(doubleValue)
162-
}
163-
default:
164-
break
165141
}
166-
default:
167-
break
142+
afterYield = closure.statements.description
168143
}
169144
}
170145
switch function.calledExpression.memberAccess?.declName.baseName.text {
@@ -177,7 +152,7 @@ extension HTMLKitUtilities {
177152
case "streamed":
178153
return .streamed(optimized: optimized, chunkSize: chunkSize)
179154
case "streamedAsync":
180-
return .streamedAsync(optimized: optimized, chunkSize: chunkSize, suspendDuration: suspendDuration)
155+
return .streamedAsync(optimized: optimized, chunkSize: chunkSize, yieldVariableName: yieldVariableName, afterYield: afterYield)
181156
default:
182157
return nil
183158
}

Sources/HTMLKitUtilities/HTMLEncoding.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
/// ```swift
2222
/// let string:StaticString = "Test"
2323
/// let _:StaticString = #html(div(string)) // ❌ promotion cannot be applied; StaticString not allowed
24-
/// let _:String = #html(div(string)) // ⚠️ promotion cannot be applied; compiles to "<div>" + String(describing: string) + "</div>"
24+
/// let _:String = #html(div(string)) // ⚠️ promotion cannot be applied; compiles to "<div>\(string)</div>"
2525
/// ```
2626
///
2727
public enum HTMLEncoding: Equatable, Sendable {

Sources/HTMLKitUtilities/HTMLExpansionContext.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public struct HTMLExpansionContext: @unchecked Sendable {
1717
public var encoding:HTMLEncoding
1818

1919
/// `HTMLResultRepresentation` of this expansion.
20-
public var representation:HTMLResultRepresentation
20+
public var representation:HTMLResultRepresentationAST
2121

2222
/// Associated attribute key responsible for the arguments.
2323
public var key:String
@@ -38,7 +38,7 @@ public struct HTMLExpansionContext: @unchecked Sendable {
3838
expansion: FreestandingMacroExpansionSyntax,
3939
ignoresCompilerWarnings: Bool,
4040
encoding: HTMLEncoding,
41-
representation: HTMLResultRepresentation,
41+
representation: HTMLResultRepresentationAST,
4242
key: String,
4343
arguments: LabeledExprListSyntax,
4444
lookupFiles: Set<String> = [],

Sources/HTMLKitUtilities/HTMLResultRepresentation.swift

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11

2-
public enum HTMLResultRepresentation: Equatable, Sendable {
2+
public enum HTMLResultRepresentation: Sendable {
33

44

55
// MARK: Literal
@@ -47,8 +47,32 @@ public enum HTMLResultRepresentation: Equatable, Sendable {
4747
/// - Parameters:
4848
/// - optimized: Whether or not to use optimized literals. Default is `true`.
4949
/// - chunkSize: The maximum size of an individual literal. Default is `1024`.
50-
/// - suspendDuration: Duration to sleep the `Task` that is yielding the stream results. Default is `nil`.
50+
/// - afterYield: Work to execute after yielding a result. The `Int` closure parameter is the index of the yielded result.
5151
/// - Returns: An `AsyncStream` of encoded literals of length up-to `chunkSize`.
52-
/// - Warning: The values are yielded synchronously in a new `Task`. Specify a `suspendDuration` to make it completely nonblocking.
53-
case streamedAsync(optimized: Bool = true, chunkSize: Int = 1024, suspendDuration: Duration? = nil)
52+
/// - Warning: The values are yielded synchronously in a new `Task`. Populate `afterYield` with async work to make it completely asynchronous.
53+
case streamedAsync(
54+
optimized: Bool = true,
55+
chunkSize: Int = 1024,
56+
_ afterYield: @Sendable (Int) async throws -> Void = { yieldIndex in }
57+
)
58+
}
59+
60+
// MARK: HTMLResultRepresentationAST
61+
public enum HTMLResultRepresentationAST: Sendable {
62+
case literal
63+
//case literalOptimized
64+
65+
case chunked(optimized: Bool = true, chunkSize: Int = 1024)
66+
67+
#if compiler(>=6.2)
68+
case chunkedInline(optimized: Bool = true, chunkSize: Int = 1024)
69+
#endif
70+
71+
case streamed(optimized: Bool = true, chunkSize: Int = 1024)
72+
case streamedAsync(
73+
optimized: Bool = true,
74+
chunkSize: Int = 1024,
75+
yieldVariableName: String? = nil,
76+
afterYield: String? = nil
77+
)
5478
}

Tests/HTMLKitTests/HTMLKitTests.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,14 @@ extension HTMLKitTests {
134134
let _:AsyncStream<String> = #html(representation: .streamedAsync(chunkSize: 3)) {
135135
div("oh yeah")
136136
}
137-
let _:AsyncStream<String> = #html(representation: .streamedAsync(suspendDuration: .milliseconds(50))) {
137+
let _:AsyncStream<String> = #html(representation: .streamedAsync({ _ in
138+
try await Task.sleep(for: .milliseconds(50))
139+
})) {
138140
div("oh yeah")
139141
}
140-
let _:AsyncStream<String> = #html(representation: .streamedAsync(chunkSize: 3, suspendDuration: .milliseconds(50))) {
142+
let _:AsyncStream<String> = #html(representation: .streamedAsync(chunkSize: 3, { _ in
143+
try await Task.sleep(for: .milliseconds(50))
144+
})) {
141145
div("oh yeah")
142146
}
143147
}

Tests/HTMLKitTests/StreamTests.swift

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@ struct StreamTests {
2323
}
2424
}
2525
)
26-
// TODO: fix infinite loop if `chunkSize` is `40`.
27-
let test:AsyncStream<String> = #html(representation: .streamedAsync(chunkSize: 50, suspendDuration: .milliseconds(5))) {
26+
var test:AsyncStream<String> = #html(
27+
representation: .streamedAsync(chunkSize: 50, { _ in
28+
try await Task.sleep(for: .milliseconds(5))
29+
})) {
2830
html {
2931
body {
3032
div()
@@ -41,13 +43,43 @@ struct StreamTests {
4143
}
4244
}
4345
var receivedHTML = ""
44-
let now = ContinuousClock.now
46+
var now = ContinuousClock.now
4547
for await test in test {
4648
receivedHTML += test
4749
}
48-
let took = ContinuousClock.now - now
50+
var took = ContinuousClock.now - now
4951
#expect(took < .milliseconds(25))
5052
#expect(receivedHTML == expected)
53+
54+
test = #html(
55+
representation: .streamedAsync(
56+
chunkSize: 40, { yieldIndex in
57+
try await Task.sleep(for: .milliseconds((yieldIndex+1) * 5))
58+
}
59+
)) {
60+
html {
61+
body {
62+
div()
63+
div()
64+
div()
65+
div()
66+
div()
67+
div()
68+
div()
69+
div()
70+
div()
71+
div()
72+
}
73+
}
74+
}
75+
receivedHTML = ""
76+
now = .now
77+
for await test in test {
78+
receivedHTML += test
79+
}
80+
took = ContinuousClock.now - now
81+
#expect(took < .milliseconds(55))
82+
#expect(receivedHTML == expected)
5183
}
5284
}
5385

0 commit comments

Comments
 (0)