Parent
#302
What to build
Implement Async.unsupervised[A](block: Async ?=> A)(using async: Async): A in yaes-core.
This is an Ox-style unsupervised scope: the block runs normally; when it completes, all still-running fibers are cancelled (not waited for); the method returns only after cancellation propagates. No new types or methods on Fiber[A] — the existing Async.fork is reused unchanged, as the supervision model is a property of the active scope stored in JvmAsync.scope, not of the fork call.
Key implementation decisions (from prototype):
def unsupervised[A](block: Async ?=> A)(using async: Async): A = {
val scope = StructuredTaskScope.open[Any, ...](Joiner.awaitAll(), ...)
val prev = JvmAsync.scope.get()
JvmAsync.scope.set(scope.asInstanceOf[StructuredTaskScope[Any, Any]])
try {
val result = block(using async)
JvmAsync.ensureJoined(scope) // interrupt trick: join() returns fast
Thread.interrupted() // clear interrupt flag
result
} catch {
case t: Throwable =>
JvmAsync.ensureJoined(scope)
Thread.interrupted() // must clear here too — mirrors Async.run
throw t
} finally {
if (prev != null) JvmAsync.scope.set(prev) // restore, do NOT remove
else JvmAsync.scope.remove()
scope.close()
}
}
Joiner.awaitAll() is chosen because its only live effect is suppressing fail-fast: it does not cancel sibling fibers when one fails, unlike awaitAllSuccessfulOrThrow(). The ensureJoined interrupt trick (set interrupt flag → join() returns immediately → close() cancels remaining) is what prevents the block from waiting for natural fiber completion. Thread.interrupted() must be called in both the happy path and the catch path to clear the flag before returning or rethrowing.
Scope save/restore: Async.unsupervised is always nested inside an existing scope, so the previous JvmAsync.scope value must be saved and restored in finally — not removed — to avoid clobbering the parent scope.
Acceptance criteria
Blocked by
None — can start immediately
Parent
#302
What to build
Implement
Async.unsupervised[A](block: Async ?=> A)(using async: Async): Ainyaes-core.This is an Ox-style unsupervised scope: the block runs normally; when it completes, all still-running fibers are cancelled (not waited for); the method returns only after cancellation propagates. No new types or methods on
Fiber[A]— the existingAsync.forkis reused unchanged, as the supervision model is a property of the active scope stored inJvmAsync.scope, not of the fork call.Key implementation decisions (from prototype):
Joiner.awaitAll()is chosen because its only live effect is suppressing fail-fast: it does not cancel sibling fibers when one fails, unlikeawaitAllSuccessfulOrThrow(). TheensureJoinedinterrupt trick (set interrupt flag →join()returns immediately →close()cancels remaining) is what prevents the block from waiting for natural fiber completion.Thread.interrupted()must be called in both the happy path and the catch path to clear the flag before returning or rethrowing.Scope save/restore:
Async.unsupervisedis always nested inside an existing scope, so the previousJvmAsync.scopevalue must be saved and restored infinally— not removed — to avoid clobbering the parent scope.Acceptance criteria
Async.unsupervisedis implemented inyaes-coreand compiles with Java 25 + Scala 3.8.1Async.forkworks insideAsync.unsupervisedwithout modificationfinallyor interrupt handler)Async.unsupervisedfollowing project conventions (brief description,@param,@return, usage example in{{{ }}})Blocked by
None — can start immediately