Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions supacode/Features/Canvas/Models/CanvasCardLayout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,10 @@ final class CanvasLayoutStore {
initiallyLoadedCardKeys = Set(stored.cardLayouts.keys)
}

// No actor-bound cleanup is needed. Avoid inferred isolated deinit, which can
// hit the TaskLocal invalid-free bug in older system Swift runtimes (swiftlang/swift#85204).
nonisolated deinit {}

func shouldAutoArrangeOnInitialEntry(for cardKeys: [String]) -> Bool {
guard !cardKeys.isEmpty else { return false }
return !cardKeys.contains { initiallyLoadedCardKeys.contains($0) }
Expand Down
33 changes: 33 additions & 0 deletions supacodeTests/CanvasLayoutStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,35 @@ import Testing

@MainActor
struct CanvasLayoutStoreTests {
@Test func releasesSynchronouslyWithTaskLocalStorageOutsideATask() async {
// Swift Testing runs in a Task. A main-queue callback reproduces synchronous
// UI teardown with thread-local TaskLocal storage instead of task-owned storage.
await withCheckedContinuation { continuation in
DispatchQueue.main.async {
MainActor.assumeIsolated {
#expect(Thread.isMainThread)
withUnsafeCurrentTask { #expect($0 == nil) }
let defaults = makeDefaults()
defer { defaults.removePersistentDomain(forName: defaultsSuiteName(defaults)) }
let layouts = ["tab-a": CanvasCardLayout(position: CGPoint(x: 10, y: 20))]

CanvasLayoutStoreTestLocal.$marker.withValue(1) {
var store: CanvasLayoutStore? = CanvasLayoutStore(defaults: defaults)
weak let releasedStore = store
store?.setCardLayouts(layouts)
store = nil

#expect(releasedStore == nil)
#expect(CanvasLayoutStoreTestLocal.marker == 1)
#expect(CanvasLayoutStore(defaults: defaults).cardLayouts == layouts)
}
#expect(CanvasLayoutStoreTestLocal.marker == 0)
continuation.resume()
}
}
}
}

@Test func loadsLegacyCardLayoutDictionary() throws {
let defaults = makeDefaults()
defer { defaults.removePersistentDomain(forName: defaultsSuiteName(defaults)) }
Expand Down Expand Up @@ -76,6 +105,10 @@ struct CanvasLayoutStoreTests {
}
}

private enum CanvasLayoutStoreTestLocal {
@TaskLocal static var marker = 0
}

private func makeDefaults() -> UserDefaults {
let suiteName = "CanvasLayoutStoreTests-\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
Expand Down
Loading