From ff6a1e62dfc390693f22ba9d70a1b620c37a6980 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 6 Sep 2026 01:02:10 +0800 Subject: [PATCH 01/14] Update GraphHost.ConstantID --- Sources/OpenSwiftUICore/Graph/GraphHost.swift | 2 +- Sources/OpenSwiftUICore/View/VariadicView/VariadicView.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/OpenSwiftUICore/Graph/GraphHost.swift b/Sources/OpenSwiftUICore/Graph/GraphHost.swift index f19ff6e98..0ffd3124f 100644 --- a/Sources/OpenSwiftUICore/Graph/GraphHost.swift +++ b/Sources/OpenSwiftUICore/Graph/GraphHost.swift @@ -207,7 +207,7 @@ open class GraphHost: CustomReflectable { package enum ConstantID: Int8, Hashable { case defaultValue - case implicitRoot + case implicitViewRoot case trueValue case defaultValue3D case failedValue diff --git a/Sources/OpenSwiftUICore/View/VariadicView/VariadicView.swift b/Sources/OpenSwiftUICore/View/VariadicView/VariadicView.swift index 6cb06de62..8ffd319ee 100644 --- a/Sources/OpenSwiftUICore/View/VariadicView/VariadicView.swift +++ b/Sources/OpenSwiftUICore/View/VariadicView/VariadicView.swift @@ -500,7 +500,7 @@ private struct MakeViewRoot: _VariadicView.ImplicitRootVisitor { var outputs: _ViewOutputs? mutating func visit(type: R.Type) where R : _VariadicView_ImplicitRoot { - let attribute = inputs.intern(R.implicitRoot, id: .implicitRoot) + let attribute = inputs.intern(R.implicitRoot, id: .implicitViewRoot) inputs.viewListOptions = R.viewListOptions outputs = R._makeView( root: _GraphValue(attribute), @@ -522,7 +522,7 @@ private struct MakeModifiedRoot: _VariadicView.ImplicitRootVisitor whe var outputs: _ViewOutputs? mutating func visit(type: R.Type) where R : _VariadicView_ImplicitRoot { - let attribute = inputs.intern(R.implicitRoot, id: .implicitRoot) + let attribute = inputs.intern(R.implicitRoot, id: .implicitViewRoot) inputs.viewListOptions = R.viewListOptions let body = body outputs = Modifier.makeDebuggableView( From 729ec95b187ddc34fa2a1223571f7f31d0c1d079 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 6 Sep 2026 01:05:15 +0800 Subject: [PATCH 02/14] Fix GraphHost.init --- Sources/OpenSwiftUICore/Graph/GraphHost.swift | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Sources/OpenSwiftUICore/Graph/GraphHost.swift b/Sources/OpenSwiftUICore/Graph/GraphHost.swift index 0ffd3124f..e21953028 100644 --- a/Sources/OpenSwiftUICore/Graph/GraphHost.swift +++ b/Sources/OpenSwiftUICore/Graph/GraphHost.swift @@ -67,6 +67,7 @@ open class GraphHost: CustomReflectable { package init() { let graph = Graph(shared: GraphHost.sharedGraph) let globalSubgraph = Subgraph(graph: graph) + let oldCurrent = Subgraph.current Subgraph.current = globalSubgraph let time = Attribute(value: Time.zero) let environment = Attribute(value: EnvironmentValues()) @@ -81,14 +82,12 @@ open class GraphHost: CustomReflectable { environment: environment, transaction: transaction ) - - let rootSubgrph = Subgraph(graph: graph) - globalSubgraph.addChild(rootSubgrph) - Subgraph.current = nil - + let rootSubgraph = Subgraph(graph: graph) + globalSubgraph.addChild(rootSubgraph) + Subgraph.current = oldCurrent self.graph = graph self.globalSubgraph = globalSubgraph - self.rootSubgraph = rootSubgrph + self.rootSubgraph = rootSubgraph self.isRemoved = false self.isHiddenForReuse = false self._time = time From 4564228c14e311670bd384bd53f6d91c0aa40087 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 6 Sep 2026 01:05:26 +0800 Subject: [PATCH 03/14] Update _openSwiftUIEmptyStub usage --- Sources/OpenSwiftUICore/Graph/GraphHost.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/OpenSwiftUICore/Graph/GraphHost.swift b/Sources/OpenSwiftUICore/Graph/GraphHost.swift index e21953028..699fe667b 100644 --- a/Sources/OpenSwiftUICore/Graph/GraphHost.swift +++ b/Sources/OpenSwiftUICore/Graph/GraphHost.swift @@ -228,10 +228,10 @@ open class GraphHost: CustomReflectable { open var graphDelegate: GraphDelegate? { nil } open var parentHost: GraphHost? { nil } - open func instantiateOutputs() {} - open func uninstantiateOutputs() {} - open func timeDidChange() {} - open func isHiddenForReuseDidChange() {} + open func instantiateOutputs() { _openSwiftUIEmptyStub() } + open func uninstantiateOutputs() { _openSwiftUIEmptyStub() } + open func timeDidChange() { _openSwiftUIEmptyStub() } + open func isHiddenForReuseDidChange() { _openSwiftUIEmptyStub() } } @_spi(ForOpenSwiftUIOnly) From c2164a5e937f76f322ae9cd69e9a73f695425cae Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 6 Sep 2026 01:13:10 +0800 Subject: [PATCH 04/14] Add id support for transaction --- Sources/OpenSwiftUICore/Graph/GraphHost.swift | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/Sources/OpenSwiftUICore/Graph/GraphHost.swift b/Sources/OpenSwiftUICore/Graph/GraphHost.swift index 699fe667b..298524b0e 100644 --- a/Sources/OpenSwiftUICore/Graph/GraphHost.swift +++ b/Sources/OpenSwiftUICore/Graph/GraphHost.swift @@ -197,7 +197,7 @@ open class GraphHost: CustomReflectable { package final func setNeedsUpdate(mayDeferUpdate: Bool, values: ViewRendererHostProperties) { self.mayDeferUpdate = self.mayDeferUpdate && mayDeferUpdate if let graph = data.graph { - // TODO: Trace + CustomEventTrace.setNeedsUpdate(values: values) graph.setNeedsUpdate() } } @@ -308,7 +308,6 @@ extension GraphHost { data.phase = phase } - // TODO: _ArchivedViewHost.reset() package final func incrementPhase() { data.phase.resetSeed.unsafeIncrement() graphDelegate?.graphDidChange() @@ -321,7 +320,7 @@ extension GraphHost { if self.removedState.isEmpty { if let parentHost { let state = parentHost.removedState - isRemoved = state.contains(.unattached) + isRemoved = state.contains(.hiddenForReuse) removedState = state } else { isRemoved = false @@ -426,7 +425,6 @@ extension GraphHost { asyncTransaction(transaction, mutation: EmptyGraphMutation()) } - // Audited for 6.5.4 package final func continueTransaction(_ body: @escaping () -> Void) { Update.assertIsLocked() var host = self @@ -439,7 +437,7 @@ extension GraphHost { } host = parent } - // TODO: CustomEventTrace + CustomEventTrace.transactionContinueAsContinuation(host) host.continuations.append(body) } @@ -456,45 +454,59 @@ extension GraphHost { for asyncTransaction in asyncTransactions { let transaction = asyncTransaction.transaction let mutations = asyncTransaction.mutations - runTransaction(transaction) { + // TODO: Forward AsyncTransaction.traceID when queue tracing is implemented. + runTransaction(transaction, do: { withTransaction(transaction) { for mutation in mutations { mutation.apply() } } - } + }, id: nil) } graphDelegate?.graphDidChange() mayDeferUpdate = true } - package final func runTransaction(_ transaction: Transaction? = nil, do body: () -> Void) { + package final func runTransaction( + _ transaction: Transaction? = nil, + do body: () -> Void, + id: UInt32? = nil + ) { instantiateIfNeeded() if let transaction, !transaction.isEmpty { data.transaction = transaction } - startTransactionUpdate() + startTransactionUpdate(id: id) body() - finishTransactionUpdate(in: globalSubgraph) + finishTransactionUpdate(in: globalSubgraph, id: id) if let transaction, !transaction.isEmpty { data.transaction = .init() } } package final func runTransaction() { - runTransaction(nil) {} + runTransaction(nil, do: {}, id: nil) } package final var needsTransaction: Bool { globalSubgraph.isDirty(flags: .transactional) } - package final func startTransactionUpdate() { + package final func startTransactionUpdate( + id: UInt32? = nil + ) { inTransaction = true + if let id { + CustomEventTrace.transactionBegin(id) + } data.transactionSeed.unsafeIncrement() } - package final func finishTransactionUpdate(in subgraph: Subgraph, postUpdate: (_ again: Bool) -> Void = { _ in }) { + package final func finishTransactionUpdate( + in subgraph: Subgraph, + postUpdate: (_ again: Bool) -> Void = { _ in }, + id: UInt32? = nil + ) { var counter = 0 repeat { let oldContinuations = continuations @@ -506,6 +518,9 @@ extension GraphHost { subgraph.update(flags: .transactional) postUpdate(!continuations.isEmpty) } while counter != 8 && !continuations.isEmpty + if let id { + CustomEventTrace.transactionEnd(id) + } inTransaction = false } } From 39d874ba687b281324921bba11aa6c0775daf063 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 6 Sep 2026 01:25:39 +0800 Subject: [PATCH 05/14] Propagate GraphHost queue trace identifiers --- Sources/OpenSwiftUICore/Graph/GraphHost.swift | 56 +++++++++---- .../GraphHostTransactionTraceTests.swift | 79 +++++++++++++++++++ 2 files changed, 120 insertions(+), 15 deletions(-) create mode 100644 Tests/OpenSwiftUICoreTests/Graph/GraphHostTransactionTraceTests.swift diff --git a/Sources/OpenSwiftUICore/Graph/GraphHost.swift b/Sources/OpenSwiftUICore/Graph/GraphHost.swift index 298524b0e..3af418625 100644 --- a/Sources/OpenSwiftUICore/Graph/GraphHost.swift +++ b/Sources/OpenSwiftUICore/Graph/GraphHost.swift @@ -10,6 +10,7 @@ import OpenSwiftUI_SPI package import OpenAttributeGraphShims import Foundation +import Synchronization // MARK: - GraphDelegate @@ -350,16 +351,17 @@ extension GraphHost { // MARK: - GraphHost + Transaction + @discardableResult package final func asyncTransaction( _ transaction: Transaction = .init(), id transactionID: Transaction.ID = Transaction.id, mutation: T, style: GraphMutation.Style = .deferred, mayDeferUpdate: Bool = true - ) where T: GraphMutation { + ) -> UInt32 where T: GraphMutation { Update.locked { guard isValid else { - return + return 0 } let shouldDeferUpdate = switch style { case .immediate: isUpdating @@ -371,12 +373,13 @@ extension GraphHost { if pendingTransactions[count-1].transactionID == transactionID, pendingTransactions[count-1].transaction.mayConcatenate(with: transaction) { pendingTransactions[count-1].append(mutation) + CustomEventTrace.transactionAppend(to: pendingTransactions[count-1].traceID) if !shouldDeferUpdate { let lastTransaction = pendingTransactions.removeLast() flushTransactions() pendingTransactions.append(lastTransaction) } - return + return pendingTransactions.last?.traceID ?? 0 } if !shouldDeferUpdate { flushTransactions() @@ -384,20 +387,23 @@ extension GraphHost { } else { graphDelegate?.beginTransaction() } - pendingTransactions.append( - AsyncTransaction( - transaction: transaction, - transactionID: transactionID, - mutations: [mutation]) + let asyncTransaction = AsyncTransaction( + transaction: transaction, + transactionID: transactionID, + mutations: [mutation] ) + CustomEventTrace.transactionEnqueue(asyncTransaction.traceID) + pendingTransactions.append(asyncTransaction) + return asyncTransaction.traceID } } + @discardableResult package final func asyncTransaction( _ transaction: Transaction = .init(), id transactionID: Transaction.ID = Transaction.id, _ body: @escaping () -> Void - ) { + ) -> UInt32 { asyncTransaction( transaction, id: transactionID, @@ -405,13 +411,14 @@ extension GraphHost { ) } + @discardableResult package final func asyncTransaction( _ transaction: Transaction = .init(), id transactionID: Transaction.ID = Transaction.id, invalidating attribute: WeakAttribute, style: GraphMutation.Style = .deferred, mayDeferUpdate: Bool = true - ) { + ) -> UInt32 { asyncTransaction( transaction, id: transactionID, @@ -421,7 +428,8 @@ extension GraphHost { ) } - package final func emptyTransaction(_ transaction: Transaction = .init()) { + @discardableResult + package final func emptyTransaction(_ transaction: Transaction = .init()) -> UInt32 { asyncTransaction(transaction, mutation: EmptyGraphMutation()) } @@ -431,7 +439,8 @@ extension GraphHost { while !host.inTransaction { guard let parent = host.parentHost else { Update.enqueueAction(reason: nil) { - host.asyncTransaction { body() } + let id = host.asyncTransaction { body() } + CustomEventTrace.transactionContinueAsNewTransaction(id) } return } @@ -454,14 +463,13 @@ extension GraphHost { for asyncTransaction in asyncTransactions { let transaction = asyncTransaction.transaction let mutations = asyncTransaction.mutations - // TODO: Forward AsyncTransaction.traceID when queue tracing is implemented. runTransaction(transaction, do: { withTransaction(transaction) { for mutation in mutations { mutation.apply() } } - }, id: nil) + }, id: asyncTransaction.traceID) } graphDelegate?.graphDidChange() mayDeferUpdate = true @@ -666,7 +674,25 @@ private struct AsyncTransaction { let transactionID: Transaction.ID - var mutations: [GraphMutation] = [] + let traceID: UInt32 + + var mutations: [GraphMutation] + + private static var nextTraceID: UInt32 = 1 + + init(transaction: Transaction, transactionID: Transaction.ID, mutations: [GraphMutation]) { + self.transaction = transaction + self.transactionID = transactionID + // [AI] 6.5.4's queue uses the incremented counter, unlike the standalone trace helper. + let oldValue = withUnsafeMutablePointer(to: &Self.nextTraceID) { pointer in + pointer.withMemoryRebound(to: Atomic.self, capacity: 1) { atomic in + atomic.pointee.wrappingAdd(2, ordering: .relaxed).oldValue + } + } + let nextValue = UInt32(Int64(Int32(bitPattern: oldValue)) + 2) + self.traceID = nextValue / 2 + 1 + self.mutations = mutations + } mutating func append(_ mutation: T) where T: GraphMutation { // NOTE: use ``Array.subscript/_modify`` instead of ``Array.last/getter`` to mutate inline diff --git a/Tests/OpenSwiftUICoreTests/Graph/GraphHostTransactionTraceTests.swift b/Tests/OpenSwiftUICoreTests/Graph/GraphHostTransactionTraceTests.swift new file mode 100644 index 000000000..24e645ead --- /dev/null +++ b/Tests/OpenSwiftUICoreTests/Graph/GraphHostTransactionTraceTests.swift @@ -0,0 +1,79 @@ +// +// GraphHostTransactionTraceTests.swift +// OpenSwiftUICoreTests + +import OpenAttributeGraphShims +@_spi(ForOpenSwiftUIOnly) @testable import OpenSwiftUICore +import Testing + +@MainActor +struct GraphHostTransactionTraceTests { + #if canImport(Darwin) + @Test + func coalescingPreservesTraceID() { + let host = GraphHost(data: .init()) + let first = host.asyncTransaction {} + let appended = host.asyncTransaction {} + #expect(first >= 2) + #expect(appended == first) + host.flushTransactions() + let next = host.asyncTransaction {} + #expect(next > first) + host.flushTransactions() + } + + @Test + func invalidHostReturnsZero() { + let host = GraphHost(data: .init()) + host.invalidate() + #expect(host.asyncTransaction {} == 0) + #expect(host.asyncTransaction(mutation: CustomGraphMutation({})) == 0) + #expect(host.asyncTransaction(invalidating: WeakAttribute()) == 0) + #expect(host.emptyTransaction() == 0) + #expect(!host.hasPendingTransactions) + } + + @Test + func immediateAppendPreservesTailAcrossFlush() { + let host = GraphHost(data: .init()) + var applied: [Int] = [] + let first = host.asyncTransaction(id: .init(value: 10)) { applied.append(1) } + let tail = host.asyncTransaction(id: .init(value: 20)) { applied.append(2) } + #expect(tail != first) + let appended = host.asyncTransaction( + id: .init(value: 20), + mutation: CustomGraphMutation { applied.append(3) }, + style: .immediate + ) + #expect(appended == tail) + #expect(applied == [1]) + host.flushTransactions() + #expect(applied == [1, 2, 3]) + } + + @Test + func wrappersReturnCoalescedID() { + let host = GraphHost(data: .init()) + let first = host.asyncTransaction(invalidating: WeakAttribute()) + #expect(first >= 2) + #expect(host.emptyTransaction() == first) + #expect(host.asyncTransaction {} == first) + host.flushTransactions() + } + + @Test + func continuationQueuesNewTransaction() { + Update.perform { + let host = GraphHost(data: .init()) + var applied = false + host.continueTransaction { applied = true } + Update.dispatchActions() + #expect(host.hasPendingTransactions) + #expect(!applied) + host.flushTransactions() + #expect(applied) + #expect(!host.hasPendingTransactions) + } + } + #endif +} From 84274b4a8abf7e2dc4247b8c212a1fc7a54ad4c6 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 6 Sep 2026 01:39:25 +0800 Subject: [PATCH 06/14] Fix.mayDeferUpdate setter --- Sources/OpenSwiftUICore/Graph/GraphHost.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/OpenSwiftUICore/Graph/GraphHost.swift b/Sources/OpenSwiftUICore/Graph/GraphHost.swift index 3af418625..4e391e9bd 100644 --- a/Sources/OpenSwiftUICore/Graph/GraphHost.swift +++ b/Sources/OpenSwiftUICore/Graph/GraphHost.swift @@ -367,7 +367,7 @@ extension GraphHost { case .immediate: isUpdating case .deferred: true } - self.mayDeferUpdate = self.mayDeferUpdate || mayDeferUpdate + self.mayDeferUpdate = self.mayDeferUpdate && mayDeferUpdate if hasPendingTransactions { let count = pendingTransactions.count if pendingTransactions[count-1].transactionID == transactionID, From 74580438068b5453d60ed3d56da93dc477cc0109 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 6 Sep 2026 03:20:48 +0800 Subject: [PATCH 07/14] Implement GraphHost global transactions and root teardown --- Sources/OpenSwiftUICore/Graph/GraphHost.swift | 93 ++++++++--- .../GraphHostGlobalTransactionTests.swift | 151 ++++++++++++++++++ .../Graph/GraphHostTests.swift | 82 ++++++++++ 3 files changed, 305 insertions(+), 21 deletions(-) create mode 100644 Tests/OpenSwiftUICoreTests/Graph/GraphHostGlobalTransactionTests.swift diff --git a/Sources/OpenSwiftUICore/Graph/GraphHost.swift b/Sources/OpenSwiftUICore/Graph/GraphHost.swift index 4e391e9bd..38c0c9115 100644 --- a/Sources/OpenSwiftUICore/Graph/GraphHost.swift +++ b/Sources/OpenSwiftUICore/Graph/GraphHost.swift @@ -2,8 +2,8 @@ // GraphHost.swift // OpenSwiftUICore // -// Audited for 6.0.87 -// Status: Blocked by transactions +// Audited for 6.5.4 +// Status: WIP // ID: 30C09FF16BC95EC5173809B57186CAC3 (SwiftUI) // ID: F9F204BD2F8DB167A76F17F3FB1B3335 (SwiftUICore) @@ -260,7 +260,9 @@ extension GraphHost { guard !isInstantiated else { return } - graphDelegate?.updateGraph { _ in } + graphDelegate?.updateGraph { _ in + _openSwiftUIEmptyStub() + } instantiateOutputs() isInstantiated = true } @@ -269,7 +271,25 @@ extension GraphHost { guard isInstantiated else { return } - _openSwiftUIUnimplementedFailure() + data.inputs.resetCaches() + uninstantiateOutputs() + rootSubgraph.willRemove() + if !data.isRemoved { + globalSubgraph.removeChild(rootSubgraph) + } + rootSubgraph.willInvalidate(isInserted: false) + if immediately { + rootSubgraph.invalidate() + } else { + Update.enqueueAction(reason: nil) { [rootSubgraph] in + rootSubgraph.invalidate() + } + } + data.rootSubgraph = Subgraph(graph: graph) + if !data.isRemoved { + globalSubgraph.addChild(rootSubgraph) + } + isInstantiated = false } package final func uninstantiate() { @@ -458,18 +478,18 @@ extension GraphHost { guard isValid, hasPendingTransactions else { return } - let asyncTransactions = pendingTransactions + let oldPendingTransactions = pendingTransactions pendingTransactions = [] - for asyncTransaction in asyncTransactions { - let transaction = asyncTransaction.transaction - let mutations = asyncTransaction.mutations + for pendingTransaction in oldPendingTransactions { + let transaction = pendingTransaction.transaction + let mutations = pendingTransaction.mutations runTransaction(transaction, do: { withTransaction(transaction) { for mutation in mutations { mutation.apply() } } - }, id: asyncTransaction.traceID) + }, id: pendingTransaction.traceID) } graphDelegate?.graphDidChange() mayDeferUpdate = true @@ -538,7 +558,18 @@ extension GraphHost { private static var pendingGlobalTransactions: [GlobalTransaction] = [] private static func flushGlobalTransactions() { - _openSwiftUIUnimplementedFailure() + guard !pendingGlobalTransactions.isEmpty else { return } + let transactions = pendingGlobalTransactions + pendingGlobalTransactions = [] + for transaction in transactions { + let base = transaction.base + if let host = transaction.hostProvider.mutationHost { + host.runTransaction(base.transaction, do: base.apply, id: base.traceID) + host.graphDelegate?.graphDidChange() + } else { + base.apply() + } + } } package static func globalTransaction( @@ -547,11 +578,35 @@ extension GraphHost { mutation: T, hostProvider: any TransactionHostProvider ) where T: GraphMutation { - _openSwiftUIUnimplementedFailure() + Update.locked { + let count = pendingGlobalTransactions.count + if count != 0 { + let didAppend = withUnsafeMutablePointer(to: &pendingGlobalTransactions[count - 1]) { last in + guard last.pointee.hostProvider === hostProvider, + last.pointee.base.transactionID == transactionID, + last.pointee.base.transaction.mayConcatenate(with: transaction) + else { return false } + last.pointee.base.append(mutation) + return true + } + if didAppend { return } + } + if count == 0 { + onMainThread { + RunLoop.addObserver(flushGlobalTransactions) + } + } + let base = AsyncTransaction( + transaction: transaction, + transactionID: transactionID, + mutations: [mutation] + ) + pendingGlobalTransactions.append(GlobalTransaction(hostProvider: hostProvider, base: base)) + } } } -// MARK: GraphHost + preference [6.5.4] +// MARK: GraphHost + preference @_spi(ForOpenSwiftUIOnly) extension GraphHost { @@ -683,7 +738,6 @@ private struct AsyncTransaction { init(transaction: Transaction, transactionID: Transaction.ID, mutations: [GraphMutation]) { self.transaction = transaction self.transactionID = transactionID - // [AI] 6.5.4's queue uses the incremented counter, unlike the standalone trace helper. let oldValue = withUnsafeMutablePointer(to: &Self.nextTraceID) { pointer in pointer.withMemoryRebound(to: Atomic.self, capacity: 1) { atomic in atomic.pointee.wrappingAdd(2, ordering: .relaxed).oldValue @@ -711,25 +765,22 @@ private struct AsyncTransaction { } } -// MARK: - GlobalTransaction [TODO] +// MARK: - GlobalTransaction -private final class GlobalTransaction { +private struct GlobalTransaction { let hostProvider: TransactionHostProvider - - init(transaction _: Transaction, hostProvider: TransactionHostProvider) { - self.hostProvider = hostProvider - } + var base: AsyncTransaction } // MARK: - Graph + GraphHost extension Graph { package func graphHost() -> GraphHost { - unsafeBitCast(context, to: GraphHost.self) + Unmanaged.fromOpaque(context!).takeUnretainedValue() } } -// MARK: - Preview [6.5.4] +// MARK: - Preview private var blockedGraphHosts: [Unmanaged] = [] // NOTE: In SwiftUI, PreviewsInjection.framework's DYLDDynamicProductLoader calls diff --git a/Tests/OpenSwiftUICoreTests/Graph/GraphHostGlobalTransactionTests.swift b/Tests/OpenSwiftUICoreTests/Graph/GraphHostGlobalTransactionTests.swift new file mode 100644 index 000000000..7387c8956 --- /dev/null +++ b/Tests/OpenSwiftUICoreTests/Graph/GraphHostGlobalTransactionTests.swift @@ -0,0 +1,151 @@ +// +// GraphHostGlobalTransactionTests.swift +// OpenSwiftUICoreTests + +import Foundation +@_spi(ForOpenSwiftUIOnly) @testable import OpenSwiftUICore +import Testing + +@MainActor +struct GraphHostGlobalTransactionTests { + #if canImport(Darwin) + @Test + func compatibleTailCombinesMutations() { + let host = TestHost(data: .init()) + let provider = Provider(host) + let values = Values() + let transaction = transaction(value: 7) + GraphHost.globalTransaction(transaction, id: .init(value: 10), mutation: SumMutation(values: values, sum: 1), hostProvider: provider) + GraphHost.globalTransaction(transaction, id: .init(value: 10), mutation: SumMutation(values: values, sum: 2), hostProvider: provider) + #expect(provider.reads == 0) + #expect(values.applied.isEmpty) + RunLoop.flushObservers() + #expect(values.applied == [3]) + #expect(provider.reads == 1) + #expect(host.data.transactionSeed == 1) + #expect(host.changes == 1) + } + + @Test + func providerIdentityIDAndCompatibilitySeparateEntries() { + let host = TestHost(data: .init()) + let first = Provider(host) + let second = Provider(host) + let values = Values() + for (provider, id, value) in [(first, UInt32(1), 7), (second, 1, 7), (second, 2, 7), (second, 2, 8)] { + GraphHost.globalTransaction(transaction(value: value), id: .init(value: id), mutation: CustomGraphMutation { + values.applied.append(Transaction.current[ValueKey.self]) + }, hostProvider: provider) + } + RunLoop.flushObservers() + #expect(values.applied == [7, 7, 7, 8]) + #expect(first.reads == 1) + #expect(second.reads == 3) + #expect(host.data.transactionSeed == 4) + #expect(host.changes == 4) + } + + @Test + func resolvesHostAtFlushWithoutDrainingItsLocalQueue() { + let host = TestHost(data: .init()) + let provider = Provider(nil) + var applied: [Int] = [] + host.asyncTransaction { applied.append(2) } + GraphHost.globalTransaction(transaction(value: 7), mutation: CustomGraphMutation { + #expect(host.inTransaction) + #expect(host.data.transaction[ValueKey.self] == 7) + #expect(Transaction.current[ValueKey.self] == 7) + applied.append(1) + }, hostProvider: provider) + provider.host = host + RunLoop.flushObservers() + #expect(applied == [1]) + #expect(host.hasPendingTransactions) + #expect(!host.inTransaction) + #expect(host.data.transaction.isEmpty) + host.flushTransactions() + #expect(applied == [1, 2]) + } + + @Test + func missingHostStillAppliesAndRestoresThreadTransaction() { + let provider = Provider(nil) + var observed: [Int] = [] + GraphHost.globalTransaction(transaction(value: 7), mutation: CustomGraphMutation { + observed.append(Transaction.current[ValueKey.self]) + }, hostProvider: provider) + withTransaction(transaction(value: 21)) { + RunLoop.flushObservers() + #expect(Transaction.current[ValueKey.self] == 21) + } + #expect(observed == [7]) + #expect(provider.reads == 1) + } + + @Test + func reentrantEnqueueRunsAfterTheCurrentSnapshot() { + let host = TestHost(data: .init()) + let provider = Provider(host) + var applied: [Int] = [] + GraphHost.globalTransaction(id: .init(value: 1), mutation: CustomGraphMutation { + applied.append(1) + GraphHost.globalTransaction(id: .init(value: 2), mutation: CustomGraphMutation { + applied.append(3) + }, hostProvider: provider) + }, hostProvider: provider) + GraphHost.globalTransaction(id: .init(value: 2), mutation: CustomGraphMutation { + applied.append(2) + }, hostProvider: provider) + RunLoop.flushObservers() + #expect(applied == [1, 2, 3]) + #expect(provider.reads == 3) + #expect(host.data.transactionSeed == 3) + } + + private func transaction(value: Int) -> Transaction { + var transaction = Transaction() + transaction[ValueKey.self] = value + return transaction + } + + private struct ValueKey: TransactionKey { + static let defaultValue = 0 + } + + private final class Values { + var applied: [Int] = [] + } + + private struct SumMutation: GraphMutation { + let values: Values + var sum: Int + + func apply() { values.applied.append(sum) } + + mutating func combine(with other: T) -> Bool where T: GraphMutation { + guard let other = other as? SumMutation, values === other.values else { return false } + sum += other.sum + return true + } + } + + private final class Provider: TransactionHostProvider { + var host: GraphHost? + var reads = 0 + var mutationHost: GraphHost? { + reads += 1 + return host + } + init(_ host: GraphHost?) { self.host = host } + } + + private final class TestHost: GraphHost, GraphDelegate { + var changes = 0 + override var graphDelegate: GraphDelegate? { self } + func updateGraph(body: (GraphHost) -> T) -> T { body(self) } + func graphDidChange() { changes += 1 } + func preferencesDidChange() {} + func beginTransaction() {} + } + #endif +} diff --git a/Tests/OpenSwiftUICoreTests/Graph/GraphHostTests.swift b/Tests/OpenSwiftUICoreTests/Graph/GraphHostTests.swift index 59966f543..ce649b55d 100644 --- a/Tests/OpenSwiftUICoreTests/Graph/GraphHostTests.swift +++ b/Tests/OpenSwiftUICoreTests/Graph/GraphHostTests.swift @@ -2,11 +2,93 @@ // GraphHostTests.swift // OpenSwiftUICoreTests +import OpenAttributeGraphShims @_spi(ForOpenSwiftUIOnly) import OpenSwiftUICore import Testing @MainActor struct GraphHostTests { + #if canImport(Darwin) + @Test + func dataRestoresCurrentSubgraph() { + let graph = Graph() + let subgraph = Subgraph(graph: graph) + let oldCurrent = Subgraph.current + Subgraph.current = subgraph + defer { Subgraph.current = oldCurrent } + + var data = GraphHost.Data() + #expect(Subgraph.current === subgraph) + data.invalidate() + } + + @Test + func transactionSeedWraps() { + let host = GraphHost(data: .init()) + host.data.transactionSeed = .max + host.startTransactionUpdate(id: 42) + #expect(host.data.transactionSeed == 0) + #expect(host.inTransaction) + host.finishTransactionUpdate(in: host.globalSubgraph, id: 42) + #expect(!host.inTransaction) + } + + @Test + func immediateUpdateRequestSurvivesDeferredMutation() { + let host = GraphHost(data: .init()) + host.asyncTransaction(mutation: CustomGraphMutation({}), mayDeferUpdate: false) + #expect(!host.mayDeferUpdate) + host.asyncTransaction(mutation: CustomGraphMutation({}), mayDeferUpdate: true) + #expect(!host.mayDeferUpdate) + host.flushTransactions() + #expect(host.mayDeferUpdate) + } + + @Test + func inheritedRemovalUsesHiddenForReuse() { + let parent = GraphHost(data: .init()) + let child = ChildHost(data: .init()) + child.parent = parent + + parent.removedState = .unattached + child.updateRemovedState() + #expect(!child.data.isRemoved) + #expect(!child.data.isHiddenForReuse) + + parent.removedState = .hiddenForReuse + child.updateRemovedState() + #expect(child.data.isRemoved) + #expect(child.data.isHiddenForReuse) + + parent.removedState = [] + child.updateRemovedState() + #expect(!child.data.isRemoved) + #expect(!child.data.isHiddenForReuse) + } + + @Test(arguments: [false, true]) + func uninstantiateReplacesRoot(immediately: Bool) { + Update.perform { + let host = GraphHost(data: .init()) + host.instantiate() + let oldRoot = host.rootSubgraph + host.uninstantiate(immediately: immediately) + #expect(!host.isInstantiated) + #expect(host.rootSubgraph !== oldRoot) + #expect(oldRoot.isValid == !immediately) + Update.dispatchActions() + #expect(!oldRoot.isValid) + host.instantiate() + #expect(host.isInstantiated) + } + } + + private final class ChildHost: GraphHost { + var parent: GraphHost? + override var parentHost: GraphHost? { parent } + } + #endif + @Test func setTimeTest() { #if canImport(Darwin) From a14519e7b858bd8d524611f018a583332f4ea752 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 6 Sep 2026 13:20:37 +0800 Subject: [PATCH 08/14] Add availability annotations to GraphHost public APIs --- Sources/OpenSwiftUICore/Graph/GraphHost.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/OpenSwiftUICore/Graph/GraphHost.swift b/Sources/OpenSwiftUICore/Graph/GraphHost.swift index 38c0c9115..3b8e8f98c 100644 --- a/Sources/OpenSwiftUICore/Graph/GraphHost.swift +++ b/Sources/OpenSwiftUICore/Graph/GraphHost.swift @@ -15,6 +15,7 @@ import Synchronization // MARK: - GraphDelegate @_spi(ForOpenSwiftUIOnly) +@available(OpenSwiftUI_v6_0, *) public protocol GraphDelegate: AnyObject { func updateGraph(body: (GraphHost) -> T) -> T func graphDidChange() @@ -23,6 +24,7 @@ public protocol GraphDelegate: AnyObject { } @_spi(ForOpenSwiftUIOnly) +@available(OpenSwiftUI_v6_0, *) extension GraphDelegate { public func beginTransaction() { onMainThread { [weak self] in @@ -41,6 +43,7 @@ extension GraphDelegate { // MARK: - GraphHost @_spi(ForOpenSwiftUIOnly) +@available(OpenSwiftUI_v6_0, *) open class GraphHost: CustomReflectable { private static let sharedGraph: Graph = { let graph = Graph() @@ -791,6 +794,7 @@ private var blockedGraphHosts: [Unmanaged] = [] // TODO: Re-enable when OpenSwiftUI implements its own preview thunk registration system. private var waitingForPreviewThunks = false // EnvironmentHelper.bool(for: "XCODE_RUNNING_FOR_PREVIEWS") +@available(OpenSwiftUI_v1_0, *) public func __previewThunksHaveFinishedLoading() { guard waitingForPreviewThunks else { return } waitingForPreviewThunks = false From cdabd4723696b0cb274b1b828092111ccdfa55be Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 6 Sep 2026 13:56:14 +0800 Subject: [PATCH 09/14] Update GraphHost.Data --- Sources/OpenSwiftUICore/Graph/GraphHost.swift | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Sources/OpenSwiftUICore/Graph/GraphHost.swift b/Sources/OpenSwiftUICore/Graph/GraphHost.swift index 3b8e8f98c..ee3ed13ed 100644 --- a/Sources/OpenSwiftUICore/Graph/GraphHost.swift +++ b/Sources/OpenSwiftUICore/Graph/GraphHost.swift @@ -106,12 +106,12 @@ open class GraphHost: CustomReflectable { package mutating func invalidate() { guard let graph else { return } - Update.begin() - globalSubgraph.invalidate() - graph.context = nil - graph.invalidate() - self.graph = nil - Update.end() + Update.perform { + globalSubgraph.invalidate() + graph.context = nil + graph.invalidate() + self.graph = nil + } } } From a275d85028e5f8672411647ed7453217a10fd286 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 6 Sep 2026 14:57:59 +0800 Subject: [PATCH 10/14] Add GraphHost extension section markers --- Sources/OpenSwiftUICore/Graph/GraphHost.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/OpenSwiftUICore/Graph/GraphHost.swift b/Sources/OpenSwiftUICore/Graph/GraphHost.swift index ee3ed13ed..548f86e11 100644 --- a/Sources/OpenSwiftUICore/Graph/GraphHost.swift +++ b/Sources/OpenSwiftUICore/Graph/GraphHost.swift @@ -242,6 +242,8 @@ open class GraphHost: CustomReflectable { @available(*, unavailable) extension GraphHost: Sendable {} +// MARK: - GraphHost + Updates [TBA] + @_spi(ForOpenSwiftUIOnly) extension GraphHost { package final func graphInvalidation(from src: AnyAttribute?) { @@ -556,6 +558,8 @@ extension GraphHost { } } +// MARK: - GraphHost + Global Transactions [TBA] + @_spi(ForOpenSwiftUIOnly) extension GraphHost { private static var pendingGlobalTransactions: [GlobalTransaction] = [] From 45a37ea8d1563674c6cf463fc996b9c038f53eae Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 6 Sep 2026 15:22:28 +0800 Subject: [PATCH 11/14] Update GraphHost --- Sources/OpenSwiftUICore/Graph/GraphHost.swift | 239 +++++++++++------- 1 file changed, 145 insertions(+), 94 deletions(-) diff --git a/Sources/OpenSwiftUICore/Graph/GraphHost.swift b/Sources/OpenSwiftUICore/Graph/GraphHost.swift index 548f86e11..368e04db7 100644 --- a/Sources/OpenSwiftUICore/Graph/GraphHost.swift +++ b/Sources/OpenSwiftUICore/Graph/GraphHost.swift @@ -3,7 +3,7 @@ // OpenSwiftUICore // // Audited for 6.5.4 -// Status: WIP +// Status: Complete (Blocked by TraceRecorder) // ID: 30C09FF16BC95EC5173809B57186CAC3 (SwiftUI) // ID: F9F204BD2F8DB167A76F17F3FB1B3335 (SwiftUICore) @@ -47,12 +47,12 @@ extension GraphDelegate { open class GraphHost: CustomReflectable { private static let sharedGraph: Graph = { let graph = Graph() - // TODO + // TODO: TraceRecorder return graph }() // MARK: - GraphHost.Data - + package struct Data { package var graph: Graph? package var globalSubgraph: Subgraph @@ -67,7 +67,7 @@ open class GraphHost: CustomReflectable { @Attribute package var updateSeed: UInt32 @Attribute package var transactionSeed: UInt32 package var inputs: _GraphInputs - + package init() { let graph = Graph(shared: GraphHost.sharedGraph) let globalSubgraph = Subgraph(graph: graph) @@ -103,7 +103,7 @@ open class GraphHost: CustomReflectable { self._transactionSeed = transactionSeed self.inputs = inputs } - + package mutating func invalidate() { guard let graph else { return } Update.perform { @@ -114,60 +114,85 @@ open class GraphHost: CustomReflectable { } } } - + package final var data: Data - package final var isValid: Bool { data.graph != nil } - package final var graph: Graph { data.graph! } - package final var graphInputs: _GraphInputs { data.inputs } - package final var globalSubgraph: Subgraph { data.globalSubgraph } - package final var rootSubgraph: Subgraph { data.rootSubgraph } + + package final var isValid: Bool { + data.graph != nil + } + + package final var graph: Graph { + data.graph! + } + + package final var graphInputs: _GraphInputs { + data.inputs + } + + package final var globalSubgraph: Subgraph { + data.globalSubgraph + } + + package final var rootSubgraph: Subgraph { + data.rootSubgraph + } + private var constants: [ConstantKey: AnyAttribute] = [:] + private(set) package final var isInstantiated: Bool = false - package final var hostPreferenceValues: WeakAttribute = WeakAttribute() + + package final var hostPreferenceValues: WeakAttribute = .init() + package final var lastHostPreferencesSeed: VersionSeed = .invalid + private final var pendingTransactions: [AsyncTransaction] = [] + package final var inTransaction: Bool = false + package final var continuations: [() -> Void] = [] + private(set) package final var mayDeferUpdate: Bool = true - + // MARK: - GraphHost.RemovedState - + package struct RemovedState: OptionSet { package let rawValue: UInt8 package init(rawValue: UInt8) { self.rawValue = rawValue } - + package static let unattached = RemovedState(rawValue: 1 << 0) - + package static let hiddenForReuse = RemovedState(rawValue: 1 << 1) } - + package final var removedState: RemovedState = [] { didSet { updateRemovedState() } } - + package static var currentHost: GraphHost { + let graph: Graph if let currentAttribute = AnyAttribute.current { - currentAttribute.graph.graphHost() + graph = currentAttribute.graph } else if let currentSubgraph = Subgraph.current { - currentSubgraph.graph.graphHost() + graph = currentSubgraph.graph } else { preconditionFailure("no current graph host") } + return graph.graphHost() } - + package init(data: Data) { mainThreadPrecondition() self.data = data graph.onUpdate { [weak self] in - guard let self, - let graphDelegate - else { return } - graphDelegate.updateGraph { _ in } + guard let self, let graphDelegate else { return } + graphDelegate.updateGraph { _ in + _openSwiftUIEmptyStub() + } } graph.onInvalidation { [weak self] attribute in guard let self else { return } @@ -175,12 +200,14 @@ open class GraphHost: CustomReflectable { } graph.context = address(of: self) } - + deinit { invalidate() - blockedGraphHosts.removeAll { $0.takeUnretainedValue() === self } + blockedGraphHosts.removeAll { + $0.takeUnretainedValue() === self + } } - + package final func invalidate() { if isInstantiated { globalSubgraph.willInvalidate(isInserted: false) @@ -188,26 +215,29 @@ open class GraphHost: CustomReflectable { } data.invalidate() } - + package static var isUpdating: Bool { sharedGraph.counter(for: .threadUpdating) != 0 } - + package final var isUpdating: Bool { - guard isValid else { return false } + guard let graph = data.graph else { + return false + } return graph.counter(for: .contextThreadUpdating) != 0 } - + package final func setNeedsUpdate(mayDeferUpdate: Bool, values: ViewRendererHostProperties) { self.mayDeferUpdate = self.mayDeferUpdate && mayDeferUpdate - if let graph = data.graph { - CustomEventTrace.setNeedsUpdate(values: values) - graph.setNeedsUpdate() + guard let graph = data.graph else { + return } + CustomEventTrace.setNeedsUpdate(values: values) + graph.setNeedsUpdate() } // MARK: - GraphHost.ConstantID - + package enum ConstantID: Int8, Hashable { case defaultValue case implicitViewRoot @@ -217,9 +247,9 @@ open class GraphHost: CustomReflectable { case placeholder case preferenceKeyDefault } - + package final func intern(_ value: T, for type: Any.Type = T.self, id: ConstantID) -> Attribute { - if let attribute = constants[ConstantKey(type: type , id: id)] { + if let attribute = constants[ConstantKey(type: type, id: id)] { return Attribute(identifier: attribute) } else { let result = globalSubgraph.apply { Attribute(value: value) } @@ -227,40 +257,58 @@ open class GraphHost: CustomReflectable { return result } } - - public final var customMirror: Mirror { Mirror(self, children: []) } - - open var graphDelegate: GraphDelegate? { nil } - open var parentHost: GraphHost? { nil } - open func instantiateOutputs() { _openSwiftUIEmptyStub() } - open func uninstantiateOutputs() { _openSwiftUIEmptyStub() } - open func timeDidChange() { _openSwiftUIEmptyStub() } - open func isHiddenForReuseDidChange() { _openSwiftUIEmptyStub() } + + public final var customMirror: Mirror { + Mirror(self, children: []) + } + + // MARK: - GraphHost Open API + + open var graphDelegate: GraphDelegate? { + nil + } + + open var parentHost: GraphHost? { + nil + } + + open func instantiateOutputs() { + _openSwiftUIEmptyStub() + } + + open func uninstantiateOutputs() { + _openSwiftUIEmptyStub() + } + + open func timeDidChange() { + _openSwiftUIEmptyStub() + } + + open func isHiddenForReuseDidChange() { + _openSwiftUIEmptyStub() + } } @_spi(ForOpenSwiftUIOnly) @available(*, unavailable) extension GraphHost: Sendable {} -// MARK: - GraphHost + Updates [TBA] +// MARK: - GraphHost + Lifecycle and Updates @_spi(ForOpenSwiftUIOnly) extension GraphHost { package final func graphInvalidation(from src: AnyAttribute?) { - guard let src else { - graphDelegate?.graphDidChange() - return - } - let host = src.graph.graphHost() - let transaction = host.data.transaction - mayDeferUpdate = mayDeferUpdate && host.mayDeferUpdate - guard !transaction.isEmpty else { - graphDelegate?.graphDidChange() - return + if let src { + let srcHost = src.graph.graphHost() + let transaction = srcHost.data.transaction + mayDeferUpdate = mayDeferUpdate && srcHost.mayDeferUpdate + if !transaction.isEmpty { + emptyTransaction(transaction) + } } - emptyTransaction(transaction) + graphDelegate?.graphDidChange() } - + package final func instantiate() { guard !isInstantiated else { return @@ -271,7 +319,7 @@ extension GraphHost { instantiateOutputs() isInstantiated = true } - + package final func uninstantiate(immediately: Bool) { guard isInstantiated else { return @@ -296,7 +344,7 @@ extension GraphHost { } isInstantiated = false } - + package final func uninstantiate() { uninstantiate(immediately: false) } @@ -313,7 +361,7 @@ extension GraphHost { instantiate() } } - + package final func setTime(_ time: Time) { guard data.time != time else { return @@ -321,43 +369,40 @@ extension GraphHost { data.time = time timeDidChange() } - + package final var environment: EnvironmentValues { data.environment } - + package final func setEnvironment(_ environment: EnvironmentValues) { data.environment = environment } - + package final func setPhase(_ phase: _GraphInputs.Phase) { data.phase = phase } - + package final func incrementPhase() { data.phase.resetSeed.unsafeIncrement() graphDelegate?.graphDidChange() } - + package final func updateRemovedState() { + var state: RemovedState let isRemoved: Bool - let removedState: RemovedState - - if self.removedState.isEmpty { + if removedState.isEmpty { if let parentHost { - let state = parentHost.removedState + state = parentHost.removedState isRemoved = state.contains(.hiddenForReuse) - removedState = state } else { + state = [] isRemoved = false - removedState = [] } } else { + state = removedState isRemoved = true - removedState = self.removedState } - let isHiddenForReuse = removedState.contains(.hiddenForReuse) - + state.formIntersection(.hiddenForReuse) if isRemoved != data.isRemoved { if isRemoved { rootSubgraph.willRemove() @@ -368,14 +413,15 @@ extension GraphHost { } data.isRemoved = isRemoved } + let isHiddenForReuse = state.contains(.hiddenForReuse) if isHiddenForReuse != data.isHiddenForReuse { data.isHiddenForReuse = isHiddenForReuse isHiddenForReuseDidChange() } } - + // MARK: - GraphHost + Transaction - + @discardableResult package final func asyncTransaction( _ transaction: Transaction = .init(), @@ -389,16 +435,21 @@ extension GraphHost { return 0 } let shouldDeferUpdate = switch style { - case .immediate: isUpdating - case .deferred: true + case .immediate: isUpdating + case .deferred: true } self.mayDeferUpdate = self.mayDeferUpdate && mayDeferUpdate if hasPendingTransactions { let count = pendingTransactions.count - if pendingTransactions[count-1].transactionID == transactionID, - pendingTransactions[count-1].transaction.mayConcatenate(with: transaction) { - pendingTransactions[count-1].append(mutation) - CustomEventTrace.transactionAppend(to: pendingTransactions[count-1].traceID) + let didAppend = withUnsafeMutablePointer(to: &pendingTransactions[count - 1]) { last in + guard last.pointee.transactionID == transactionID, + last.pointee.transaction.mayConcatenate(with: transaction) + else { return false } + last.pointee.append(mutation) + CustomEventTrace.transactionAppend(to: last.pointee.traceID) + return true + } + if didAppend { if !shouldDeferUpdate { let lastTransaction = pendingTransactions.removeLast() flushTransactions() @@ -422,7 +473,7 @@ extension GraphHost { return asyncTransaction.traceID } } - + @discardableResult package final func asyncTransaction( _ transaction: Transaction = .init(), @@ -435,7 +486,7 @@ extension GraphHost { mutation: CustomGraphMutation(body) ) } - + @discardableResult package final func asyncTransaction( _ transaction: Transaction = .init(), @@ -452,7 +503,7 @@ extension GraphHost { mayDeferUpdate: mayDeferUpdate ) } - + @discardableResult package final func emptyTransaction(_ transaction: Transaction = .init()) -> UInt32 { asyncTransaction(transaction, mutation: EmptyGraphMutation()) @@ -474,7 +525,7 @@ extension GraphHost { CustomEventTrace.transactionContinueAsContinuation(host) host.continuations.append(body) } - + package final var hasPendingTransactions: Bool { !pendingTransactions.isEmpty } @@ -516,15 +567,15 @@ extension GraphHost { data.transaction = .init() } } - + package final func runTransaction() { runTransaction(nil, do: {}, id: nil) } - + package final var needsTransaction: Bool { globalSubgraph.isDirty(flags: .transactional) } - + package final func startTransactionUpdate( id: UInt32? = nil ) { @@ -558,7 +609,7 @@ extension GraphHost { } } -// MARK: - GraphHost + Global Transactions [TBA] +// MARK: - GraphHost + Global Transactions @_spi(ForOpenSwiftUIOnly) extension GraphHost { @@ -783,7 +834,7 @@ private struct GlobalTransaction { extension Graph { package func graphHost() -> GraphHost { - Unmanaged.fromOpaque(context!).takeUnretainedValue() + unsafeBitCast(context!, to: GraphHost.self) } } From 5ae708636bea7293169e336f727dc6a33b9f7c3a Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 6 Sep 2026 21:10:16 +0800 Subject: [PATCH 12/14] Fix continueTransaction --- Sources/OpenSwiftUICore/Graph/GraphHost.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/OpenSwiftUICore/Graph/GraphHost.swift b/Sources/OpenSwiftUICore/Graph/GraphHost.swift index 368e04db7..9629f7ebf 100644 --- a/Sources/OpenSwiftUICore/Graph/GraphHost.swift +++ b/Sources/OpenSwiftUICore/Graph/GraphHost.swift @@ -515,7 +515,7 @@ extension GraphHost { while !host.inTransaction { guard let parent = host.parentHost else { Update.enqueueAction(reason: nil) { - let id = host.asyncTransaction { body() } + let id = self.asyncTransaction { body() } CustomEventTrace.transactionContinueAsNewTransaction(id) } return From 4d4c23954b9b2d63408c5fe9d9708f231189618a Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 6 Sep 2026 22:30:17 +0800 Subject: [PATCH 13/14] Update GraphHost + preference --- Sources/OpenSwiftUICore/Graph/GraphHost.swift | 125 ++++++++++-------- 1 file changed, 70 insertions(+), 55 deletions(-) diff --git a/Sources/OpenSwiftUICore/Graph/GraphHost.swift b/Sources/OpenSwiftUICore/Graph/GraphHost.swift index 9629f7ebf..f14966ab9 100644 --- a/Sources/OpenSwiftUICore/Graph/GraphHost.swift +++ b/Sources/OpenSwiftUICore/Graph/GraphHost.swift @@ -664,37 +664,37 @@ extension GraphHost { } } -// MARK: GraphHost + preference +// MARK: - GraphHost + preference @_spi(ForOpenSwiftUIOnly) extension GraphHost { package final func addPreference(_ key: K.Type) where K: HostPreferenceKey { Graph.withoutUpdate { - data.hostPreferenceKeys.add(key) + data.hostPreferenceKeys.add(K.self) } } - + package final func removePreference(_ key: K.Type) where K: HostPreferenceKey { Graph.withoutUpdate { - data.hostPreferenceKeys.remove(key) + data.hostPreferenceKeys.remove(K.self) } } - + package final func preferenceValues() -> PreferenceValues { instantiateIfNeeded() return hostPreferenceValues.value ?? PreferenceValues() } - + package final func preferenceValue(_ key: K.Type) -> K.Value where K: HostPreferenceKey { - if data.hostPreferenceKeys.contains(key) { - return preferenceValues()[key].value + if data.hostPreferenceKeys.contains(K.self) { + return preferenceValues()[K.self].value } else { - defer { removePreference(key) } - addPreference(key) - return preferenceValues()[key].value + defer { removePreference(K.self) } + addPreference(K.self) + return preferenceValues()[K.self].value } } - + package final func updatePreferences() -> Bool { let seed = hostPreferenceValues.value?.seed ?? .empty let didUpdate = !seed.matches(lastHostPreferencesSeed) @@ -703,22 +703,6 @@ extension GraphHost { } } -// MARK: - ConstantKey - -private struct ConstantKey: Hashable { - static func == (lhs: ConstantKey, rhs: ConstantKey) -> Bool { - lhs.type == rhs.type && lhs.id == rhs.id - } - - func hash(into hasher: inout Hasher) { - hasher.combine(ObjectIdentifier(type)) - hasher.combine(id.rawValue) - } - - var type: Any.Type - var id: GraphHost.ConstantID -} - // MARK: - GraphMutation package protocol GraphMutation { @@ -726,7 +710,7 @@ package protocol GraphMutation { func apply() - mutating func combine(with other: T) -> Bool where T: GraphMutation + mutating func combine(with other: some GraphMutation) -> Bool } // MARK: GraphMutation.Style @@ -740,37 +724,17 @@ package enum _GraphMutation_Style { package struct CustomGraphMutation: GraphMutation { let body: () -> Void + package init(_ body: @escaping () -> Void) { self.body = body } - package func apply() { body() } - package func combine(with other: T) -> Bool where T: GraphMutation { false } -} - -// MARK: - InvalidatingGraphMutation -struct InvalidatingGraphMutation: GraphMutation { - let attribute: AnyWeakAttribute - - func apply() { - attribute.attribute?.invalidateValue() - } - - func combine(with mutation: some GraphMutation) -> Bool { - guard let mutation = mutation as? InvalidatingGraphMutation else { - return false - } - return mutation.attribute == attribute + package func apply() { + body() } -} - -// MARK: - EmptyGraphMutation -private struct EmptyGraphMutation: GraphMutation { - package init() {} - package func apply() {} package func combine(with other: T) -> Bool where T: GraphMutation { - T.self == EmptyGraphMutation.self + false } } @@ -806,14 +770,14 @@ private struct AsyncTransaction { self.mutations = mutations } - mutating func append(_ mutation: T) where T: GraphMutation { + mutating func append(_ mutation: some GraphMutation) { // NOTE: use ``Array.subscript/_modify`` instead of ``Array.last/getter`` to mutate inline guard mutations.isEmpty || !mutations[mutations.count - 1].combine(with: mutation) else { return } mutations.append(mutation) } - + func apply() { withTransaction(transaction) { for mutation in mutations { @@ -823,6 +787,57 @@ private struct AsyncTransaction { } } +// MARK: - ConstantKey + +private struct ConstantKey: Hashable { + var type: Any.Type + + var id: GraphHost.ConstantID + + static func == (lhs: ConstantKey, rhs: ConstantKey) -> Bool { + lhs.type == rhs.type && lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(type)) + hasher.combine(id.rawValue) + } +} + + +// MARK: - InvalidatingGraphMutation + +struct InvalidatingGraphMutation: GraphMutation { + let attribute: AnyWeakAttribute + + func apply() { + attribute.attribute?.invalidateValue() + } + + func combine(with mutation: some GraphMutation) -> Bool { + guard let mutation = mutation as? InvalidatingGraphMutation else { + return false + } + return mutation.attribute == attribute + } +} + +// MARK: - EmptyGraphMutation + +private struct EmptyGraphMutation: GraphMutation { + init() { + _openSwiftUIEmptyStub() + } + + func apply() { + _openSwiftUIEmptyStub() + } + + func combine(with other: T) -> Bool { + T.self == EmptyGraphMutation.self + } +} + // MARK: - GlobalTransaction private struct GlobalTransaction { From b07e6589ac5f04f60dc49974fafbe11ff6836106 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 6 Sep 2026 23:47:49 +0800 Subject: [PATCH 14/14] Update GraphHostTests --- .../GraphHostGlobalTransactionTests.swift | 151 ---------- .../Graph/GraphHostTests.swift | 270 ++++++++++++++++-- .../GraphHostTransactionTraceTests.swift | 79 ----- 3 files changed, 241 insertions(+), 259 deletions(-) delete mode 100644 Tests/OpenSwiftUICoreTests/Graph/GraphHostGlobalTransactionTests.swift delete mode 100644 Tests/OpenSwiftUICoreTests/Graph/GraphHostTransactionTraceTests.swift diff --git a/Tests/OpenSwiftUICoreTests/Graph/GraphHostGlobalTransactionTests.swift b/Tests/OpenSwiftUICoreTests/Graph/GraphHostGlobalTransactionTests.swift deleted file mode 100644 index 7387c8956..000000000 --- a/Tests/OpenSwiftUICoreTests/Graph/GraphHostGlobalTransactionTests.swift +++ /dev/null @@ -1,151 +0,0 @@ -// -// GraphHostGlobalTransactionTests.swift -// OpenSwiftUICoreTests - -import Foundation -@_spi(ForOpenSwiftUIOnly) @testable import OpenSwiftUICore -import Testing - -@MainActor -struct GraphHostGlobalTransactionTests { - #if canImport(Darwin) - @Test - func compatibleTailCombinesMutations() { - let host = TestHost(data: .init()) - let provider = Provider(host) - let values = Values() - let transaction = transaction(value: 7) - GraphHost.globalTransaction(transaction, id: .init(value: 10), mutation: SumMutation(values: values, sum: 1), hostProvider: provider) - GraphHost.globalTransaction(transaction, id: .init(value: 10), mutation: SumMutation(values: values, sum: 2), hostProvider: provider) - #expect(provider.reads == 0) - #expect(values.applied.isEmpty) - RunLoop.flushObservers() - #expect(values.applied == [3]) - #expect(provider.reads == 1) - #expect(host.data.transactionSeed == 1) - #expect(host.changes == 1) - } - - @Test - func providerIdentityIDAndCompatibilitySeparateEntries() { - let host = TestHost(data: .init()) - let first = Provider(host) - let second = Provider(host) - let values = Values() - for (provider, id, value) in [(first, UInt32(1), 7), (second, 1, 7), (second, 2, 7), (second, 2, 8)] { - GraphHost.globalTransaction(transaction(value: value), id: .init(value: id), mutation: CustomGraphMutation { - values.applied.append(Transaction.current[ValueKey.self]) - }, hostProvider: provider) - } - RunLoop.flushObservers() - #expect(values.applied == [7, 7, 7, 8]) - #expect(first.reads == 1) - #expect(second.reads == 3) - #expect(host.data.transactionSeed == 4) - #expect(host.changes == 4) - } - - @Test - func resolvesHostAtFlushWithoutDrainingItsLocalQueue() { - let host = TestHost(data: .init()) - let provider = Provider(nil) - var applied: [Int] = [] - host.asyncTransaction { applied.append(2) } - GraphHost.globalTransaction(transaction(value: 7), mutation: CustomGraphMutation { - #expect(host.inTransaction) - #expect(host.data.transaction[ValueKey.self] == 7) - #expect(Transaction.current[ValueKey.self] == 7) - applied.append(1) - }, hostProvider: provider) - provider.host = host - RunLoop.flushObservers() - #expect(applied == [1]) - #expect(host.hasPendingTransactions) - #expect(!host.inTransaction) - #expect(host.data.transaction.isEmpty) - host.flushTransactions() - #expect(applied == [1, 2]) - } - - @Test - func missingHostStillAppliesAndRestoresThreadTransaction() { - let provider = Provider(nil) - var observed: [Int] = [] - GraphHost.globalTransaction(transaction(value: 7), mutation: CustomGraphMutation { - observed.append(Transaction.current[ValueKey.self]) - }, hostProvider: provider) - withTransaction(transaction(value: 21)) { - RunLoop.flushObservers() - #expect(Transaction.current[ValueKey.self] == 21) - } - #expect(observed == [7]) - #expect(provider.reads == 1) - } - - @Test - func reentrantEnqueueRunsAfterTheCurrentSnapshot() { - let host = TestHost(data: .init()) - let provider = Provider(host) - var applied: [Int] = [] - GraphHost.globalTransaction(id: .init(value: 1), mutation: CustomGraphMutation { - applied.append(1) - GraphHost.globalTransaction(id: .init(value: 2), mutation: CustomGraphMutation { - applied.append(3) - }, hostProvider: provider) - }, hostProvider: provider) - GraphHost.globalTransaction(id: .init(value: 2), mutation: CustomGraphMutation { - applied.append(2) - }, hostProvider: provider) - RunLoop.flushObservers() - #expect(applied == [1, 2, 3]) - #expect(provider.reads == 3) - #expect(host.data.transactionSeed == 3) - } - - private func transaction(value: Int) -> Transaction { - var transaction = Transaction() - transaction[ValueKey.self] = value - return transaction - } - - private struct ValueKey: TransactionKey { - static let defaultValue = 0 - } - - private final class Values { - var applied: [Int] = [] - } - - private struct SumMutation: GraphMutation { - let values: Values - var sum: Int - - func apply() { values.applied.append(sum) } - - mutating func combine(with other: T) -> Bool where T: GraphMutation { - guard let other = other as? SumMutation, values === other.values else { return false } - sum += other.sum - return true - } - } - - private final class Provider: TransactionHostProvider { - var host: GraphHost? - var reads = 0 - var mutationHost: GraphHost? { - reads += 1 - return host - } - init(_ host: GraphHost?) { self.host = host } - } - - private final class TestHost: GraphHost, GraphDelegate { - var changes = 0 - override var graphDelegate: GraphDelegate? { self } - func updateGraph(body: (GraphHost) -> T) -> T { body(self) } - func graphDidChange() { changes += 1 } - func preferencesDidChange() {} - func beginTransaction() {} - } - #endif -} diff --git a/Tests/OpenSwiftUICoreTests/Graph/GraphHostTests.swift b/Tests/OpenSwiftUICoreTests/Graph/GraphHostTests.swift index ce649b55d..7b4d9e227 100644 --- a/Tests/OpenSwiftUICoreTests/Graph/GraphHostTests.swift +++ b/Tests/OpenSwiftUICoreTests/Graph/GraphHostTests.swift @@ -2,13 +2,14 @@ // GraphHostTests.swift // OpenSwiftUICoreTests +import Foundation import OpenAttributeGraphShims -@_spi(ForOpenSwiftUIOnly) import OpenSwiftUICore +@_spi(ForOpenSwiftUIOnly) @testable import OpenSwiftUICore import Testing @MainActor +@Suite(.disabled(if: attributeGraphVendor == .oag)) struct GraphHostTests { - #if canImport(Darwin) @Test func dataRestoresCurrentSubgraph() { let graph = Graph() @@ -24,13 +25,12 @@ struct GraphHostTests { @Test func transactionSeedWraps() { - let host = GraphHost(data: .init()) - host.data.transactionSeed = .max - host.startTransactionUpdate(id: 42) - #expect(host.data.transactionSeed == 0) - #expect(host.inTransaction) - host.finishTransactionUpdate(in: host.globalSubgraph, id: 42) - #expect(!host.inTransaction) + Update.perform { + let host = GraphHost(data: .init()) + host.data.transactionSeed = .max + host.runTransaction() + #expect(host.data.transactionSeed == 0) + } } @Test @@ -53,17 +53,20 @@ struct GraphHostTests { parent.removedState = .unattached child.updateRemovedState() #expect(!child.data.isRemoved) - #expect(!child.data.isHiddenForReuse) + #expect(child.hiddenStates.isEmpty) parent.removedState = .hiddenForReuse child.updateRemovedState() #expect(child.data.isRemoved) - #expect(child.data.isHiddenForReuse) + #expect(child.hiddenStates == [true]) + + child.updateRemovedState() + #expect(child.hiddenStates == [true]) parent.removedState = [] child.updateRemovedState() #expect(!child.data.isRemoved) - #expect(!child.data.isHiddenForReuse) + #expect(child.hiddenStates == [true, false]) } @Test(arguments: [false, true]) @@ -83,27 +86,236 @@ struct GraphHostTests { } } + @Test + func continuationWithoutActiveAncestorQueuesOnOriginalHost() { + Update.perform { + let parent = GraphHost(data: .init()) + let child = ChildHost(data: .init()) + child.parent = parent + var applied = 0 + + child.continueTransaction { applied += 1 } + Update.dispatchActions() + #expect(applied == 0) + + parent.flushTransactions() + #expect(applied == 0) + child.flushTransactions() + #expect(applied == 1) + } + } + + @Test + func setTimeNotifiesOnlyWhenValueChanges() { + let host = TimeHost(data: .init()) + host.setTime(.zero) + #expect(host.observedTimes.isEmpty) + + host.setTime(Time(seconds: 1)) + host.setTime(Time(seconds: 1)) + #expect(host.observedTimes == [1]) + + host.setTime(Time(seconds: 2)) + #expect(host.observedTimes == [1, 2]) + } + + @MainActor + @Suite + struct GraphHostTransactionTraceTests { + @Test + func coalescingPreservesTraceID() { + let host = GraphHost(data: .init()) + let first = host.asyncTransaction {} + let appended = host.asyncTransaction {} + #expect(first != 0) + #expect(appended == first) + host.flushTransactions() + let next = host.asyncTransaction {} + #expect(next != first) + host.flushTransactions() + } + + @Test + func invalidHostRejectsMutation() { + let host = GraphHost(data: .init()) + var applied = false + host.invalidate() + let id = host.asyncTransaction(mutation: CustomGraphMutation { applied = true }) + #expect(id == 0) + #expect(!host.hasPendingTransactions) + host.flushTransactions() + #expect(!applied) + } + + @Test + func immediateAppendPreservesTailAcrossFlush() { + let host = GraphHost(data: .init()) + var applied: [Int] = [] + let first = host.asyncTransaction(id: .init(value: 10)) { applied.append(1) } + let tail = host.asyncTransaction(id: .init(value: 20)) { applied.append(2) } + #expect(tail != first) + let appended = host.asyncTransaction( + id: .init(value: 20), + mutation: CustomGraphMutation { applied.append(3) }, + style: .immediate + ) + #expect(appended == tail) + #expect(applied == [1]) + host.flushTransactions() + #expect(applied == [1, 2, 3]) + } + } + + @MainActor + @Suite + struct GraphHostGlobalTransactionTests { + @Test + func compatibleTailCombinesMutations() { + let host = TestHost(data: .init()) + let provider = Provider(host) + let values = Values() + let transaction = transaction(value: 7) + GraphHost.globalTransaction(transaction, id: .init(value: 10), mutation: SumMutation(values: values, sum: 1), hostProvider: provider) + GraphHost.globalTransaction(transaction, id: .init(value: 10), mutation: SumMutation(values: values, sum: 2), hostProvider: provider) + #expect(provider.reads == 0) + #expect(values.applied.isEmpty) + RunLoop.flushObservers() + #expect(values.applied == [3]) + #expect(host.changes == 1) + } + + @Test(arguments: [ + (false, UInt32(1), 7), // Different provider. + (true, UInt32(2), 7), // Different transaction ID. + (true, UInt32(1), 8), // Incompatible transaction values. + ]) + func incompatibleTailKeepsMutationsSeparate(sameProvider: Bool, secondID: UInt32, secondValue: Int) { + let host = TestHost(data: .init()) + let first = Provider(host) + let second = sameProvider ? first : Provider(host) + let values = Values() + let firstTransaction = transaction(value: 7) + let secondTransaction = secondValue == 7 ? firstTransaction : transaction(value: secondValue) + GraphHost.globalTransaction(firstTransaction, id: .init(value: 1), mutation: SumMutation(values: values, sum: 1), hostProvider: first) + GraphHost.globalTransaction(secondTransaction, id: .init(value: secondID), mutation: SumMutation(values: values, sum: 2), hostProvider: second) + RunLoop.flushObservers() + #expect(values.applied == [1, 2]) + } + + @Test + func resolvesHostAtFlushWithoutDrainingItsLocalQueue() { + let host = TestHost(data: .init()) + let provider = Provider(nil) + var applied: [Int] = [] + host.asyncTransaction { applied.append(2) } + GraphHost.globalTransaction(transaction(value: 7), mutation: CustomGraphMutation { + #expect(host.data.transaction[ValueKey.self] == 7) + #expect(Transaction.current[ValueKey.self] == 7) + applied.append(1) + }, hostProvider: provider) + provider.host = host + RunLoop.flushObservers() + #expect(applied == [1]) + #expect(host.data.transaction.isEmpty) + host.flushTransactions() + #expect(applied == [1, 2]) + } + + @Test + func missingHostStillAppliesAndRestoresThreadTransaction() { + let provider = Provider(nil) + var observed: [Int] = [] + GraphHost.globalTransaction(transaction(value: 7), mutation: CustomGraphMutation { + observed.append(Transaction.current[ValueKey.self]) + }, hostProvider: provider) + withTransaction(transaction(value: 21)) { + RunLoop.flushObservers() + #expect(Transaction.current[ValueKey.self] == 21) + } + #expect(observed == [7]) + } + + @Test + func reentrantEnqueueRunsAfterTheCurrentSnapshot() { + let host = TestHost(data: .init()) + let provider = Provider(host) + var applied: [Int] = [] + GraphHost.globalTransaction(id: .init(value: 1), mutation: CustomGraphMutation { + applied.append(1) + GraphHost.globalTransaction(id: .init(value: 2), mutation: CustomGraphMutation { + applied.append(3) + }, hostProvider: provider) + }, hostProvider: provider) + GraphHost.globalTransaction(id: .init(value: 2), mutation: CustomGraphMutation { + applied.append(2) + }, hostProvider: provider) + RunLoop.flushObservers() + #expect(applied == [1, 2, 3]) + } + + private func transaction(value: Int) -> Transaction { + var transaction = Transaction() + transaction[ValueKey.self] = value + return transaction + } + + private struct ValueKey: TransactionKey { + static let defaultValue = 0 + } + + private final class Values { + var applied: [Int] = [] + } + + private struct SumMutation: GraphMutation { + let values: Values + var sum: Int + + func apply() { values.applied.append(sum) } + + mutating func combine(with other: T) -> Bool where T: GraphMutation { + guard let other = other as? SumMutation, values === other.values else { return false } + sum += other.sum + return true + } + } + + private final class Provider: TransactionHostProvider { + var host: GraphHost? + var reads = 0 + var mutationHost: GraphHost? { + reads += 1 + return host + } + init(_ host: GraphHost?) { self.host = host } + } + + private final class TestHost: GraphHost, GraphDelegate { + var changes = 0 + override var graphDelegate: GraphDelegate? { self } + func updateGraph(body: (GraphHost) -> T) -> T { body(self) } + func graphDidChange() { changes += 1 } + func preferencesDidChange() {} + func beginTransaction() {} + } + } + private final class ChildHost: GraphHost { var parent: GraphHost? + var hiddenStates: [Bool] = [] + override var parentHost: GraphHost? { parent } + + override func isHiddenForReuseDidChange() { + hiddenStates.append(data.isHiddenForReuse) + } } - #endif - @Test - func setTimeTest() { - #if canImport(Darwin) - let graphHost = GraphHost(data: .init()) - #expect(graphHost.data.time.seconds == 0.0) - - graphHost.setTime(Time.zero) - #expect(graphHost.data.time.seconds == 0.0) - - graphHost.setTime(Time.infinity) - #expect(graphHost.data.time.seconds == Time.infinity.seconds) - - let timeNow = Time.systemUptime - graphHost.setTime(timeNow) - #expect(graphHost.data.time.seconds == timeNow.seconds) - #endif + private final class TimeHost: GraphHost { + var observedTimes: [Double] = [] + + override func timeDidChange() { + observedTimes.append(data.time.seconds) + } } } diff --git a/Tests/OpenSwiftUICoreTests/Graph/GraphHostTransactionTraceTests.swift b/Tests/OpenSwiftUICoreTests/Graph/GraphHostTransactionTraceTests.swift deleted file mode 100644 index 24e645ead..000000000 --- a/Tests/OpenSwiftUICoreTests/Graph/GraphHostTransactionTraceTests.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// GraphHostTransactionTraceTests.swift -// OpenSwiftUICoreTests - -import OpenAttributeGraphShims -@_spi(ForOpenSwiftUIOnly) @testable import OpenSwiftUICore -import Testing - -@MainActor -struct GraphHostTransactionTraceTests { - #if canImport(Darwin) - @Test - func coalescingPreservesTraceID() { - let host = GraphHost(data: .init()) - let first = host.asyncTransaction {} - let appended = host.asyncTransaction {} - #expect(first >= 2) - #expect(appended == first) - host.flushTransactions() - let next = host.asyncTransaction {} - #expect(next > first) - host.flushTransactions() - } - - @Test - func invalidHostReturnsZero() { - let host = GraphHost(data: .init()) - host.invalidate() - #expect(host.asyncTransaction {} == 0) - #expect(host.asyncTransaction(mutation: CustomGraphMutation({})) == 0) - #expect(host.asyncTransaction(invalidating: WeakAttribute()) == 0) - #expect(host.emptyTransaction() == 0) - #expect(!host.hasPendingTransactions) - } - - @Test - func immediateAppendPreservesTailAcrossFlush() { - let host = GraphHost(data: .init()) - var applied: [Int] = [] - let first = host.asyncTransaction(id: .init(value: 10)) { applied.append(1) } - let tail = host.asyncTransaction(id: .init(value: 20)) { applied.append(2) } - #expect(tail != first) - let appended = host.asyncTransaction( - id: .init(value: 20), - mutation: CustomGraphMutation { applied.append(3) }, - style: .immediate - ) - #expect(appended == tail) - #expect(applied == [1]) - host.flushTransactions() - #expect(applied == [1, 2, 3]) - } - - @Test - func wrappersReturnCoalescedID() { - let host = GraphHost(data: .init()) - let first = host.asyncTransaction(invalidating: WeakAttribute()) - #expect(first >= 2) - #expect(host.emptyTransaction() == first) - #expect(host.asyncTransaction {} == first) - host.flushTransactions() - } - - @Test - func continuationQueuesNewTransaction() { - Update.perform { - let host = GraphHost(data: .init()) - var applied = false - host.continueTransaction { applied = true } - Update.dispatchActions() - #expect(host.hasPendingTransactions) - #expect(!applied) - host.flushTransactions() - #expect(applied) - #expect(!host.hasPendingTransactions) - } - } - #endif -}