WeakAsyncSequence avoids implicit reference type capturing when using a for-in loop with an AsyncSequence.
Without WeakAsyncSequence 👻
let stream: AsyncStream<Int> = ...
func doSomething() { ... }
let task = Task { [weak self] in
guard let self else { return }
for await i in stream { // Capturing self and it might be caused memory leaks.
doSomething()
}
}
With WeakAsyncSequence ✅
let stream: AsyncStream<Int> = ...
func doSomething() { ... }
let task = Task { [weak self] in
for try await (i, `self`) in try self.weak(\.stream) { // Receives Element and self without memory leaks.
self.doSomething()
}
}
Simply add the following line to your Package.swift
:
.package(url: "https://github.com/marty-suzuki/WeakAsyncSequence.git", from: "0.1.0")
This is a case of a memory leak.
final class Leaks: Sendable {
private let asyncStream = AsyncStream<Int> { continuation in
Task {
for i in 1...10 {
continuation.yield(i)
try await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC)
}
continuation.finish()
}
}
private let spy: Spy
init(_ spy: Spy) {
self.spy = spy
spy.add("init")
}
deinit {
spy.add("deinit")
}
func doTask() {
Task { [weak self, spy] in
guard let self else {
return
}
for try await i in asyncStream {
spy.add("i = \(i) with \(self)")
}
}
}
}
@Test func leaksAndNotCalledDeinit() async throws {
let spy = Spy()
var object: Leaks? = Leaks(spy)
let name = "\(object!)"
object?.doTask()
try await Task.sleep(nanoseconds: 3 * NSEC_PER_SEC)
object = nil
let expected = [
"init",
"i = 1 with \(name)",
"i = 2 with \(name)",
"i = 3 with \(name)",
// The deinit was not called, therefore it means a memory leak occurred.
]
#expect(spy.values == expected)
}
This is a case of no memory leaks.
final class NoLeaks: Sendable {
private let asyncStream = AsyncStream<Int> { continuation in
Task {
for i in 1...10 {
continuation.yield(i)
try await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC)
}
continuation.finish()
}
}
private let spy: Spy
init(_ spy: Spy) {
self.spy = spy
spy.add("init")
}
deinit {
spy.add("deinit")
}
func doTask() {
Task { [weak self, spy] in
for try await (i, `self`) in try self.weak(\.asyncStream) {
spy.add("i = \(i) with \(self)")
}
}
}
}
@Test func noLeaksAndCalledDeinit() async throws {
let spy = Spy()
var object: NoLeaks? = NoLeaks(spy)
let name = "\(object!)"
object?.doTask()
try await Task.sleep(nanoseconds: 3 * NSEC_PER_SEC)
object = nil
let expected = [
"init",
"i = 1 with \(name)",
"i = 2 with \(name)",
"i = 3 with \(name)",
"deinit",
]
#expect(spy.values == expected)
}