Skip to content

Commit

Permalink
Issue 443: Add typed sink allowing sinks to be reused across render p…
Browse files Browse the repository at this point in the history
…asses.

When the UI does not update synchronously from a render pass, it may send
actions to the previous sink. If the same type of sink was declared on the
subsequent render pass, proxy the action to the new event pipe.

Fixes #443
  • Loading branch information
Dave Apgar committed Jul 23, 2019
1 parent b7f9d09 commit d6e9477
Show file tree
Hide file tree
Showing 2 changed files with 454 additions and 20 deletions.
99 changes: 92 additions & 7 deletions swift/Workflow/Sources/SubtreeManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ extension WorkflowNode {
/// Sinks from the outside world (i.e. UI)
private var eventPipes: [EventPipe] = []

/// Typed sinks from the previous render pass
private var previousSinks: [ObjectIdentifier:AnyTypedSink] = [:]

/// The current array of children
private (set) internal var childWorkflows: [ChildKey:AnyChildWorkflow] = [:]

Expand All @@ -34,6 +37,7 @@ extension WorkflowNode {

/// Create a workflow context containing the existing children
let context = Context(
previousSinks: previousSinks,
originalChildWorkflows: childWorkflows,
originalChildWorkers: childWorkers)

Expand All @@ -52,8 +56,12 @@ extension WorkflowNode {
/// Merge all of the signals together from the subscriptions.
self.subscriptions = Subscriptions(eventSources: context.eventSources, eventPipe: EventPipe())

/// Captured the typed sinks from this render pass to allow reuse.
self.previousSinks = context.sinkStore.usedSinks

/// Capture all the pipes to be enabled after render completes.
self.eventPipes = context.eventPipes
self.eventPipes.append(contentsOf: context.sinkStore.eventPipes())
self.eventPipes.append(self.subscriptions.eventPipe)

/// Set all event pipes to `pending`.
Expand Down Expand Up @@ -119,12 +127,16 @@ extension WorkflowNode.SubtreeManager {

}

// MARK: - Render Context

extension WorkflowNode.SubtreeManager {

/// The workflow context implementation used by the subtree manager.
fileprivate final class Context: RenderContextType {

private (set) internal var eventPipes: [EventPipe]

private (set) internal var sinkStore: SinkStore

private let originalChildWorkflows: [ChildKey:AnyChildWorkflow]
private (set) internal var usedChildWorkflows: [ChildKey:AnyChildWorkflow]
Expand All @@ -134,9 +146,11 @@ extension WorkflowNode.SubtreeManager {

private (set) internal var eventSources: [Signal<AnyWorkflowAction<WorkflowType>, NoError>] = []

internal init(originalChildWorkflows: [ChildKey:AnyChildWorkflow], originalChildWorkers: [AnyChildWorker]) {
internal init(previousSinks: [ObjectIdentifier:AnyTypedSink], originalChildWorkflows: [ChildKey:AnyChildWorkflow], originalChildWorkers: [AnyChildWorker]) {
self.eventPipes = []

self.sinkStore = SinkStore(previousSinks: previousSinks)

self.originalChildWorkflows = originalChildWorkflows
self.usedChildWorkflows = [:]

Expand Down Expand Up @@ -191,13 +205,12 @@ extension WorkflowNode.SubtreeManager {

func makeSink<Action>(of actionType: Action.Type) -> Sink<Action> where Action : WorkflowAction, WorkflowType == Action.WorkflowType {

let eventPipe = EventPipe()
let typedSink = sinkStore.findOrCreate(actionType: Action.self)

let sink = Sink<Action> { action in
let event = Output.update(AnyWorkflowAction(action), source: .external)
eventPipe.handle(event: event)
typedSink.handle(action: action)
}
eventPipes.append(eventPipe)

return sink
}

Expand Down Expand Up @@ -227,6 +240,70 @@ extension WorkflowNode.SubtreeManager {
}


// MARK: - Reusable Sink

extension WorkflowNode.SubtreeManager {
fileprivate struct SinkStore {
private var previousSinks: [ObjectIdentifier:AnyTypedSink]
private (set) var usedSinks: [ObjectIdentifier:AnyTypedSink]

init(previousSinks: [ObjectIdentifier:AnyTypedSink]) {
self.previousSinks = previousSinks
self.usedSinks = [:]
}

mutating func findOrCreate<Action: WorkflowAction>(actionType: Action.Type) -> TypedSink<Action> {
let key = ObjectIdentifier(actionType)

let typedSink: TypedSink<Action>

if let previousSink = previousSinks.removeValue(forKey: key) as? TypedSink<Action> {
// Reused a previous sink, creating a new event pipe to send the action through.
previousSink.eventPipe = EventPipe()
typedSink = previousSink
} else if let usedSink = usedSinks[key] as? TypedSink<Action> {
// Multiple sinks using the same backing sink.
typedSink = usedSink
} else {
// Create a new typed sink.
typedSink = TypedSink<Action>()
}

usedSinks[key] = typedSink

return typedSink
}

func eventPipes() -> [EventPipe] {
let eventPipes = usedSinks.values.map { typedSink -> EventPipe in
typedSink.eventPipe
}

return eventPipes
}
}

fileprivate class AnyTypedSink {
var eventPipe: EventPipe

init() {
eventPipe = EventPipe()
}
}

fileprivate final class TypedSink<Action: WorkflowAction>: AnyTypedSink where Action.WorkflowType == WorkflowType {

func handle(action: Action) {
let output = Output.update(AnyWorkflowAction(action), source: .external)

eventPipe.handle(event: output)
}
}
}


// MARK: - EventPipe

extension WorkflowNode.SubtreeManager {
fileprivate final class EventPipe {

Expand Down Expand Up @@ -295,6 +372,8 @@ extension WorkflowNode.SubtreeManager {
}
}

// MARK: - ChildKey

extension WorkflowNode.SubtreeManager {

struct ChildKey: Hashable {
Expand All @@ -320,6 +399,8 @@ extension WorkflowNode.SubtreeManager {

}

// MARK: - Workers

extension WorkflowNode.SubtreeManager {

/// Abstract base class for running children in the subtree.
Expand Down Expand Up @@ -372,6 +453,8 @@ extension WorkflowNode.SubtreeManager {
}


// MARK: - Subscriptions

extension WorkflowNode.SubtreeManager {
fileprivate final class Subscriptions {
private var (lifetime, token) = Lifetime.make()
Expand All @@ -395,6 +478,8 @@ extension WorkflowNode.SubtreeManager {
}


// MARK: - Child Workflows

extension WorkflowNode.SubtreeManager {

/// Abstract base class for running children in the subtree.
Expand All @@ -415,9 +500,9 @@ extension WorkflowNode.SubtreeManager {
}

}

fileprivate final class ChildWorkflow<W: Workflow>: AnyChildWorkflow {

private let node: WorkflowNode<W>
private var outputMap: (W.Output) -> AnyWorkflowAction<WorkflowType>

Expand Down
Loading

0 comments on commit d6e9477

Please sign in to comment.