Skip to content

[refactor]: introduce a 'HostContext' for exposing host info through tree #325

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 12, 2025
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
42 changes: 25 additions & 17 deletions Workflow/Sources/SubtreeManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,19 @@ extension WorkflowNode {

private let session: WorkflowSession

private let observer: WorkflowObserver?
/// Reference to the context object for the entity hosting the corresponding node.
private let hostContext: HostContext

private var observer: WorkflowObserver? {
hostContext.observer
}

init(
session: WorkflowSession,
observer: WorkflowObserver? = nil
hostContext: HostContext
) {
self.session = session
self.observer = observer
self.hostContext = hostContext
}

/// Performs an update pass using the given closure.
Expand All @@ -60,8 +65,8 @@ extension WorkflowNode {
previousSinks: previousSinks,
originalChildWorkflows: childWorkflows,
originalSideEffectLifetimes: sideEffectLifetimes,
session: session,
observer: observer
hostContext: hostContext,
session: session
)

let wrapped = RenderContext.make(implementation: context)
Expand Down Expand Up @@ -148,18 +153,21 @@ extension WorkflowNode.SubtreeManager {
private let originalSideEffectLifetimes: [AnyHashable: SideEffectLifetime]
private(set) var usedSideEffectLifetimes: [AnyHashable: SideEffectLifetime]

private let hostContext: HostContext
private let session: WorkflowSession
private let observer: WorkflowObserver?

private var observer: WorkflowObserver? {
hostContext.observer
}

init(
previousSinks: [ObjectIdentifier: AnyReusableSink],
originalChildWorkflows: [ChildKey: AnyChildWorkflow],
originalSideEffectLifetimes: [AnyHashable: SideEffectLifetime],
session: WorkflowSession,
observer: WorkflowObserver?
hostContext: HostContext,
session: WorkflowSession
) {
self.eventPipes = []

self.sinkStore = SinkStore(previousSinks: previousSinks)

self.originalChildWorkflows = originalChildWorkflows
Expand All @@ -168,8 +176,8 @@ extension WorkflowNode.SubtreeManager {
self.originalSideEffectLifetimes = originalSideEffectLifetimes
self.usedSideEffectLifetimes = [:]

self.hostContext = hostContext
self.session = session
self.observer = observer
}

func render<Child, Action>(
Expand All @@ -194,7 +202,7 @@ extension WorkflowNode.SubtreeManager {
let eventPipe = EventPipe()
eventPipes.append(eventPipe)

/// See if we can
/// See if we can reuse an existing child node for the given key.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this truncated sentence has bothered me for a long time – i think this is probably what it was going to

if let existing = originalChildWorkflows[childKey] {
/// Cast the untyped child into a specific typed child. Because our children are keyed by their workflow
/// type, this should never fail.
Expand All @@ -217,8 +225,8 @@ extension WorkflowNode.SubtreeManager {
outputMap: { outputMap($0) },
eventPipe: eventPipe,
key: key,
parentSession: session,
observer: observer
hostContext: hostContext,
parentSession: session
)
}

Expand Down Expand Up @@ -453,15 +461,15 @@ extension WorkflowNode.SubtreeManager {
outputMap: @escaping (W.Output) -> any WorkflowAction<WorkflowType>,
eventPipe: EventPipe,
key: String,
parentSession: WorkflowSession?,
observer: WorkflowObserver?
hostContext: HostContext,
parentSession: WorkflowSession?
) {
self.outputMap = outputMap
self.node = WorkflowNode<W>(
workflow: workflow,
key: key,
parentSession: parentSession,
observer: observer
hostContext: hostContext,
parentSession: parentSession
)

super.init(eventPipe: eventPipe)
Expand Down
37 changes: 31 additions & 6 deletions Workflow/Sources/WorkflowHost.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ public protocol WorkflowDebugger {

/// Manages an active workflow hierarchy.
public final class WorkflowHost<WorkflowType: Workflow> {
private let debugger: WorkflowDebugger?

private let (outputEvent, outputEventObserver) = Signal<WorkflowType.Output, Never>.pipe()

// @testable
Expand All @@ -45,6 +43,13 @@ public final class WorkflowHost<WorkflowType: Workflow> {
/// as state transitions occur within the hierarchy.
public let rendering: Property<WorkflowType.Rendering>

/// Context object to pass down to descendant nodes in the tree.
let context: HostContext

private var debugger: WorkflowDebugger? {
context.debugger
}

/// Initializes a new host with the given workflow at the root.
///
/// - Parameter workflow: The root workflow in the hierarchy
Expand All @@ -56,17 +61,20 @@ public final class WorkflowHost<WorkflowType: Workflow> {
observers: [WorkflowObserver] = [],
debugger: WorkflowDebugger? = nil
) {
self.debugger = debugger

let observer = WorkflowObservation
.sharedObserversInterceptor
.workflowObservers(for: observers)
.chained()

self.context = HostContext(
observer: observer,
debugger: debugger
)

self.rootNode = WorkflowNode(
workflow: workflow,
parentSession: nil,
observer: observer
hostContext: context,
parentSession: nil
)

self.mutableRendering = MutableProperty(rootNode.render())
Expand Down Expand Up @@ -115,3 +123,20 @@ public final class WorkflowHost<WorkflowType: Workflow> {
outputEvent
}
}

// MARK: - HostContext

/// A context object to expose certain root-level information to each node
/// in the Workflow tree.
final class HostContext {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

class?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, are you asking why is it a class? assuming 'yes', then it's because i think we will eventually want to allow different parts of the tree to be able to mutate some of its properties and have those changes be visible elsewhere. e.g. you're handling an action and want to remember that 'some node in the tree was invalidated' by the time the root/host has to do the next render.

let observer: WorkflowObserver?
let debugger: WorkflowDebugger?

init(
observer: WorkflowObserver?,
debugger: WorkflowDebugger?
) {
self.observer = observer
self.debugger = debugger
}
}
30 changes: 18 additions & 12 deletions Workflow/Sources/WorkflowNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,47 +16,53 @@

/// Manages a running workflow.
final class WorkflowNode<WorkflowType: Workflow> {
/// Holds the current state of the workflow
/// The current `State` of the node's `Workflow`.
private var state: WorkflowType.State

/// Holds the current workflow.
/// Holds the current `Workflow` managed by this node.
private var workflow: WorkflowType

/// An optional `WorkflowObserver` instance
let observer: WorkflowObserver?

var onOutput: ((Output) -> Void)?
/// Reference to the context object for the entity hosting this node.
let hostContext: HostContext

/// Manages the children of this workflow, including diffs during/after render passes.
private let subtreeManager: SubtreeManager

/// 'Session' metadata associated with this node
let session: WorkflowSession

/// Callback to invoke when a child `Output` is produced.
var onOutput: ((Output) -> Void)?

/// An optional `WorkflowObserver` instance
var observer: WorkflowObserver? {
hostContext.observer
}

init(
workflow: WorkflowType,
key: String = "",
parentSession: WorkflowSession? = nil,
observer: WorkflowObserver? = nil
hostContext: HostContext,
parentSession: WorkflowSession? = nil
) {
/// Get the initial state
self.workflow = workflow
self.observer = observer
self.hostContext = hostContext
self.session = WorkflowSession(
workflow: workflow,
renderKey: key,
parent: parentSession
)
self.subtreeManager = SubtreeManager(
session: session,
observer: observer
hostContext: hostContext
)

self.observer?.sessionDidBegin(session)
hostContext.observer?.sessionDidBegin(session)

self.state = workflow.makeInitialState()

self.observer?.workflowDidMakeInitialState(
observer?.workflowDidMakeInitialState(
workflow,
initialState: state,
session: session
Expand Down
2 changes: 1 addition & 1 deletion Workflow/Tests/SubtreeManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ extension WorkflowNode.SubtreeManager {
fileprivate convenience init() {
self.init(
session: .testing(),
observer: nil
hostContext: .testing()
)
}
}
16 changes: 15 additions & 1 deletion Workflow/Tests/TestUtilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
*/

import Foundation
import Workflow

@testable import Workflow

/// Renders to a model that contains a callback, which in turn sends an output event.
struct StateTransitioningWorkflow: Workflow {
Expand Down Expand Up @@ -55,3 +56,16 @@ struct StateTransitioningWorkflow: Workflow {
}
}
}

// MARK: -

extension HostContext {
static func testing(
observer: WorkflowObserver? = nil
) -> HostContext {
HostContext(
observer: observer,
debugger: nil
)
}
}
18 changes: 18 additions & 0 deletions Workflow/Tests/WorkflowNodeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -400,3 +400,21 @@ private class SessionCollectingObserver: WorkflowObserver {
#else
extension Never: Equatable {}
#endif

// MARK: -

extension WorkflowNode {
convenience init(
workflow: WorkflowType,
key: String = "",
parentSession: WorkflowSession? = nil,
observer: WorkflowObserver? = nil
) {
self.init(
workflow: workflow,
key: key,
hostContext: HostContext.testing(observer: observer),
parentSession: parentSession
)
}
}
Loading