Skip to content

Commit

Permalink
Merge branch 'master' into mobius-loop-rewrite
Browse files Browse the repository at this point in the history
  • Loading branch information
JensAyton committed Apr 21, 2020
2 parents f3543e4 + c39dc39 commit 7bf37d7
Show file tree
Hide file tree
Showing 12 changed files with 68 additions and 80 deletions.
8 changes: 4 additions & 4 deletions Mobius.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@
2D3A6972235737430053C95E /* WorkBag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkBag.swift; sourceTree = "<group>"; };
2D3A69752359EAA00053C95E /* WorkBagTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkBagTests.swift; sourceTree = "<group>"; };
2D3EEB9523FADA9E006E478A /* AsyncStartStopStateMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncStartStopStateMachine.swift; sourceTree = "<group>"; };
2D3F26EC237B02B8004C2B75 /* AsyncDispatchQueueConnectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AsyncDispatchQueueConnectable.swift; path = ConnectableConvenienceClasses/AsyncDispatchQueueConnectable.swift; sourceTree = "<group>"; };
2D3F26EC237B02B8004C2B75 /* AsyncDispatchQueueConnectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncDispatchQueueConnectable.swift; sourceTree = "<group>"; };
2D58735F238EC60F001F21ED /* EventRouterDisposalLogicalRaceRegressionTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventRouterDisposalLogicalRaceRegressionTest.swift; sourceTree = "<group>"; };
2DA1E89A2449EF6E00D240B7 /* BeginnerLoop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeginnerLoop.swift; sourceTree = "<group>"; };
2DA1E89D2449F1ED00D240B7 /* WikiTutorialTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WikiTutorialTest.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -569,15 +569,15 @@
);
sourceTree = "<group>";
};
5B1F1038210F5E7D0067193C /* ConnectableConvenienceClasses */ = {
5B1F1038210F5E7D0067193C /* Deprecated */ = {
isa = PBXGroup;
children = (
5B1F103B210F5EBC0067193C /* ActionConnectable.swift */,
5B1F1039210F5E9A0067193C /* BlockingFunctionConnectable.swift */,
5B1F1042210F5F590067193C /* ClosureConnectable.swift */,
5B1F103E210F5EE40067193C /* ConsumerConnectable.swift */,
);
path = ConnectableConvenienceClasses;
path = Deprecated;
sourceTree = "<group>";
};
5B1F1045211036FC0067193C /* ConnectableConvenienceClassesTests */ = {
Expand Down Expand Up @@ -692,7 +692,7 @@
isa = PBXGroup;
children = (
02D0DDBC2366EF8D00A1CE4C /* EffectHandlers */,
5B1F1038210F5E7D0067193C /* ConnectableConvenienceClasses */,
5B1F1038210F5E7D0067193C /* Deprecated */,
5BB287BC209995410043B530 /* Disposables */,
5B4A369821107D0E00279C7D /* EventSources */,
2D3EEB9523FADA9E006E478A /* AsyncStartStopStateMachine.swift */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import Foundation
/// Base class for creating an action based `connectable`.
///
/// Invoking the `connection` functions will block the current thread until done.
// swiftlint:disable:next line_length
@available(*, deprecated, message: "ActionConnectable will be removed before Mobius 1.0. If you’re using it with EffectRouter, you probably don’t need an explicit connectable, just route to your action function.")
open class ActionConnectable<Input, Output>: Connectable {
private var innerConnectable: ClosureConnectable<Input, Output>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import Foundation
/// Base class for creating a function based `connectable`.
///
/// Invoking the `connection` functions will block the current thread until done.
// swiftlint:disable:next line_length
@available(*, deprecated, message: "BlockingFunctionConnectable will be removed before Mobius 1.0. If you’re using it with EffectRouter, you probably don’t need an explicit connectable, just route to your fire-and-forget function.")
open class BlockingFunctionConnectable<Input, Output>: Connectable {
private var innerConnectable: ClosureConnectable<Input, Output>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,36 +20,19 @@
import Foundation

final class ClosureConnectable<Input, Output>: Connectable {
private var queue: DispatchQueue?
private var output: Consumer<Output>?
private let closure: (Input) -> Output?
private let lock = Lock()

// If the closure produces output, it will be passed to the consumer. If it doesnt, it wont (see `connect`).
init(_ closure: @escaping (Input) -> Output?, queue: DispatchQueue? = nil) {
self.closure = closure
self.queue = queue
}

init(_ outputClosure: @escaping (Input) -> Output, queue: DispatchQueue? = nil) {
init(_ outputClosure: @escaping (Input) -> Output) {
closure = outputClosure
self.queue = queue
}

init(_ noOutputClosure: @escaping (Input) -> Void, queue: DispatchQueue? = nil) {
init(_ noOutputClosure: @escaping (Input) -> Void) {
closure = { input in
noOutputClosure(input)
return nil
}
self.queue = queue
}

init(_ nothing: @escaping () -> Void, queue: DispatchQueue? = nil) {
closure = { _ in
nothing()
return nil
}
self.queue = queue
}

private func dispatchInput(_ input: Input, consumer: @escaping Consumer<Output>) {
Expand All @@ -66,13 +49,7 @@ final class ClosureConnectable<Input, Output>: Connectable {
return Connection(
acceptClosure: { input in
if let consumer = self.output {
if let queue = self.queue {
queue.async {
self.dispatchInput(input, consumer: consumer)
}
} else {
self.dispatchInput(input, consumer: consumer)
}
self.dispatchInput(input, consumer: consumer)
}
},
disposeClosure: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import Foundation
/// Base class for creating a consumer based `connectable`.
///
/// Invoking the `connection` functions will block the current thread until done.
// swiftlint:disable:next line_length
@available(*, deprecated, message: "ConsumerConnectable will be removed before Mobius 1.0. If you’re using it with EffectRouter, you probably don’t need an explicit connectable, just route to your consumer function.")
open class ConsumerConnectable<Input, Output>: Connectable {
private var innerConnectable: ClosureConnectable<Input, Output>

Expand Down
82 changes: 43 additions & 39 deletions MobiusCore/Source/LoggingAdaptors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,51 +17,55 @@
// specific language governing permissions and limitations
// under the License.

/// Helper to wrap initator functions with log calls.
///
/// Also adds call stack annotation where we call into the client-provided initiator.
final class LoggingInitiate<Model, Effect> {
typealias Initiate = MobiusCore.Initiate<Model, Effect>
typealias First = MobiusCore.First<Model, Effect>

private let realInit: Initiate
private let willInit: (Model) -> Void
private let didInit: (Model, First) -> Void
typealias UpdateClosure<Model, Event, Effect> = (Model, Event) -> Next<Model, Effect>

init<Logger: MobiusLogger>(_ realInit: @escaping Initiate, logger: Logger)
where Logger.Model == Model, Logger.Effect == Effect {
self.realInit = realInit
willInit = logger.willInitiate
didInit = logger.didInitiate
extension MobiusLogger {
/// Wraps an Initiate in logging calls and stack annotations
func wrap(initiate: @escaping Initiate<Model, Effect>) -> Initiate<Model, Effect> {
return { model in
self.willInitiate(model: model)
let result = invokeInitiate(initiate, model: model)
self.didInitiate(model: model, first: result)
return result
}
}

func initiate(_ model: Model) -> First {
willInit(model)
let result = invokeInitiate(model: model)
didInit(model, result)

return result
/// Wraps an update closure in logging calls and stack annotations
func wrap(update: @escaping UpdateClosure<Model, Event, Effect>) -> UpdateClosure<Model, Event, Effect> {
return { model, event in
self.willUpdate(model: model, event: event)
let result = invokeUpdate(update, model: model, event: event)
self.didUpdate(model: model, event: event, next: result)
return result
}
}

@inline(never)
@_silgen_name("__MOBIUS_IS_CALLING_AN_INITIATOR_FUNCTION__")
private func invokeInitiate(model: Model) -> First {
return realInit(model)
/// Wraps an Update in logging calls and stack annotations
func wrap(update: Update<Model, Event, Effect>) -> Update<Model, Event, Effect> {
return Update(wrap(update: update.updateClosure))
}
}

extension Update {
/// Helper to wrap update functions with log calls.
///
/// Also adds call stack annotation where we call into the client-provided update.
@inline(never)
@_silgen_name("__MOBIUS_IS_CALLING_AN_UPDATE_FUNCTION__")
func logging<L: MobiusLogger>(_ logger: L) -> Update where L.Model == Model, L.Event == Event, L.Effect == Effect {
return Update { model, event in
logger.willUpdate(model: model, event: event)
let next = self.update(model: model, event: event)
logger.didUpdate(model: model, event: event, next: next)
return next
}
}
/// Invoke an initiate function, leaving a hint on the stack.
///
/// To work as intended, this function must be exactly like this. `@_silgen_name` can’t be used on a closure,
/// for example.
@inline(never)
@_silgen_name("__MOBIUS_IS_CALLING_AN_INITIATOR_FUNCTION__")
private func invokeInitiate<Model, Effect>(_ initiate: Initiate<Model, Effect>, model: Model) -> First<Model, Effect> {
return initiate(model)
}

/// Invoke an update function, leaving a hint on the stack.
///
/// To work as intended, this function must be exactly like this. `@_silgen_name` can’t be used on a closure,
/// for example.
@inline(never)
@_silgen_name("__MOBIUS_IS_CALLING_AN_UPDATE_FUNCTION__")
private func invokeUpdate<Model, Event, Effect>(
_ update: @escaping (Model, Event) -> Next<Model, Effect>,
model: Model,
event: Event
) -> Next<Model, Effect> {
return update(model, event)
}
7 changes: 4 additions & 3 deletions MobiusCore/Source/Mobius.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,17 @@ import Foundation
///
/// [update function]: https://github.com/spotify/Mobius.swift/wiki/Concepts#update-function
public struct Update<Model, Event, Effect> {
private let update: (Model, Event) -> Next<Model, Effect>
@usableFromInline let updateClosure: (Model, Event) -> Next<Model, Effect>

/// Creates an `Update` struct wrapping the provided function.
public init(_ update: @escaping (Model, Event) -> Next<Model, Effect>) {
self.update = update
self.updateClosure = update
}

/// Invokes the update function.
@inlinable
public func update(model: Model, event: Event) -> Next<Model, Effect> {
return self.update(model, event)
return updateClosure(model, event)
}

/// Invokes the update function.
Expand Down
2 changes: 1 addition & 1 deletion MobiusCore/Source/MobiusController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ public final class MobiusController<Model, Event, Effect> {
// Wrap initiator (if any) in a logger
let actualInitiate: Initiate<Model, Effect>
if let initiate = initiate {
actualInitiate = LoggingInitiate(initiate, logger: logger).initiate
actualInitiate = logger.wrap(initiate: initiate)
} else {
actualInitiate = { First(model: $0) }
}
Expand Down
4 changes: 2 additions & 2 deletions MobiusCore/Source/MobiusLoop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public final class MobiusLoop<Model, Event, Effect>: Disposable {
effects: [Effect],
logger: AnyMobiusLogger<Model, Event, Effect>
) {
let loggingUpdate = update.logging(logger)
let loggingUpdate = logger.wrap(update: update.updateClosure)

let workBag = WorkBag(accessGuard: access)
self.workBag = workBag
Expand All @@ -69,7 +69,7 @@ public final class MobiusLoop<Model, Event, Effect>: Disposable {
// This is an unowned read of `self`, but at this point `self` is being kept alive by the local
// `processNext`.
let model = self.model
processNext(loggingUpdate.update(model: model, event: event))
processNext(loggingUpdate(model, event))
}
workBag.service()
}
Expand Down
8 changes: 4 additions & 4 deletions MobiusCore/Test/LoggingInitiateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,21 @@ class LoggingInitiateTests: QuickSpec {
override func spec() {
describe("LoggingInitiate") {
var logger: TestMobiusLogger!
var loggingInitiate: LoggingInitiate<String, String>!
var loggingInitiate: Initiate<String, String>!

beforeEach {
logger = TestMobiusLogger()
loggingInitiate = LoggingInitiate({ model in First(model: model) }, logger: logger)
loggingInitiate = logger.wrap { model in First(model: model) }
}

it("should log willInitiate and didInitiate for each initiate attempt") {
_ = loggingInitiate.initiate("from this")
_ = loggingInitiate("from this")

expect(logger.logMessages).to(equal(["willInitiate(from this)", "didInitiate(from this, First<String, String>(model: \"from this\", effects: []))"]))
}

it("should return init from delegate") {
let first = loggingInitiate.initiate("hey")
let first = loggingInitiate("hey")

expect(first.model).to(equal("hey"))
expect(first.effects).to(beEmpty())
Expand Down
2 changes: 1 addition & 1 deletion MobiusCore/Test/LoggingUpdateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class LoggingUpdateTests: QuickSpec {

beforeEach {
logger = TestMobiusLogger()
loggingUpdate = Update { model, event in Next(model: model, effects: [event]) }.logging(logger)
loggingUpdate = logger.wrap(update: Update { model, event in Next(model: model, effects: [event]) })
}

it("should log willUpdate and didUpdate for each update attempt") {
Expand Down

0 comments on commit 7bf37d7

Please sign in to comment.