diff --git a/core/shared/src/main/scala/fs2/internal/CompileScope.scala b/core/shared/src/main/scala/fs2/internal/CompileScope.scala index bdf3704a53..7e720fa31a 100644 --- a/core/shared/src/main/scala/fs2/internal/CompileScope.scala +++ b/core/shared/src/main/scala/fs2/internal/CompileScope.scala @@ -25,6 +25,10 @@ import fs2.internal.CompileScope.InterruptContext * For example, `s.chunks` is defined with `s.repeatPull` which in turn is defined with `Pull.loop(...).stream`. * In this case, a single scope is created as a result of the call to `.stream`. * + * The `root` scope is special is that is closed by the top level compile loop, rather than by instructions in the + * Stream structure. This is to allow extending the lifetime of Stream when compiling to a `cats.effect.Resource` + * (not to be confused with `fs2.internal.Resource`). + * * Scopes may also be opened and closed manually with `Stream#scope`. For the stream `s.scope`, a scope * is opened before evaluation of `s` and closed once `s` finishes evaluation. * @@ -86,9 +90,20 @@ private[fs2] final class CompileScope[F[_]] private ( * Invoked when a resource is released during the scope's lifetime. * When the action returns, the resource may not be released yet, as * it may have been `leased` to other scopes. + * + * Note: + * + * This method is a no-op for the `root` scope, because we want to + * close it through the toplevel compile loop, rather than by + * instructions in the stream structure. + * + * This is to allow extending the lifetime of Stream when compiling + * to a `cats.effect.Resource` (not to be confused with + * `fs2.internal.Resource`). + * */ def releaseResource(id: Token, ec: ExitCase[Throwable]): F[Either[Throwable, Unit]] = - if (!parent.isEmpty) { // TODO comment to explain this + if (!parent.isEmpty) { F.flatMap(state.modify { _.unregisterResource(id) }) { case Some(resource) => resource.release(ec) case None => F.pure(Right(())) // resource does not exist in scope any more.