Skip to content

Commit fc261ce

Browse files
committed
Prototype: making EventSource a Connectable<M, E>
1 parent 315a69a commit fc261ce

File tree

4 files changed

+131
-9
lines changed

4 files changed

+131
-9
lines changed

MobiusCore/Source/Mobius.swift

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ public enum Mobius {
7474
return Builder(
7575
update: update,
7676
effectHandler: effectHandler,
77-
eventSource: AnyEventSource({ _ in AnonymousDisposable(disposer: {}) }),
77+
eventSource: AnyConnectable { _ in .init(acceptClosure: { _ in }, disposeClosure: {}) },
7878
eventConsumerTransformer: { $0 },
7979
logger: AnyMobiusLogger(NoopLogger())
8080
)
@@ -104,14 +104,14 @@ public enum Mobius {
104104
public struct Builder<Model, Event, Effect> {
105105
private let update: Update<Model, Event, Effect>
106106
private let effectHandler: AnyConnectable<Effect, Event>
107-
private let eventSource: AnyEventSource<Event>
107+
private let eventSource: AnyConnectable<Model, Event>
108108
private let logger: AnyMobiusLogger<Model, Event, Effect>
109109
private let eventConsumerTransformer: ConsumerTransformer<Event>
110110

111111
fileprivate init<EffectHandler: Connectable>(
112112
update: Update<Model, Event, Effect>,
113113
effectHandler: EffectHandler,
114-
eventSource: AnyEventSource<Event>,
114+
eventSource: AnyConnectable<Model, Event>,
115115
eventConsumerTransformer: @escaping ConsumerTransformer<Event>,
116116
logger: AnyMobiusLogger<Model, Event, Effect>
117117
) where EffectHandler.Input == Effect, EffectHandler.Output == Event {
@@ -140,7 +140,35 @@ public enum Mobius {
140140
return Builder(
141141
update: update,
142142
effectHandler: effectHandler,
143-
eventSource: AnyEventSource(eventSource),
143+
eventSource: AnyConnectable { consumer in
144+
var disposable: Disposable? = eventSource.subscribe(consumer: consumer)
145+
return .init(acceptClosure: { _ in // EventSource ignores the Model (value)
146+
}, disposeClosure: {
147+
disposable?.dispose()
148+
disposable = nil
149+
})
150+
},
151+
eventConsumerTransformer: eventConsumerTransformer,
152+
logger: logger
153+
)
154+
}
155+
156+
/// TODO
157+
/*
158+
@return a new {@link Builder} with the supplied {@link Connectable<M,E>}, and the same values
159+
as the current one for the other fields. NOTE: Invoking this method will replace the
160+
current event source with the supplied one. If a loop has a {@link Connectable<M,E>} as
161+
its event source, it will connect to it and will invoke the {@link Connection<M>} accept
162+
method every time the model changes. This allows us to conditionally subscribe to
163+
different sources based on the current state. If you provide a regular {@link
164+
EventSource<E>}, it will be wrapped in a {@link Connectable} and that implementation will
165+
subscribe to that event source only once when the loop is initialized.
166+
*/
167+
public func withEventSource<Source: Connectable>(_ eventSource: Source) -> Builder where Source.Input == Model, Source.Output == Event {
168+
return Builder(
169+
update: update,
170+
effectHandler: effectHandler,
171+
eventSource: AnyConnectable(eventSource),
144172
eventConsumerTransformer: eventConsumerTransformer,
145173
logger: logger
146174
)

MobiusCore/Source/MobiusLoop.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public final class MobiusLoop<Model, Event, Effect>: Disposable {
2525
private var workBag: WorkBag
2626

2727
private var effectConnection: Connection<Effect>! = nil
28+
private var eventSourceConnection: Connection<Model>! = nil
2829
private var consumeEvent: Consumer<Event>! = nil
2930
private let modelPublisher: ConnectablePublisher<Model>
3031

@@ -35,7 +36,7 @@ public final class MobiusLoop<Model, Event, Effect>: Disposable {
3536
init(
3637
model: Model,
3738
update: Update<Model, Event, Effect>,
38-
eventSource: AnyEventSource<Event>,
39+
eventSource: AnyConnectable<Model, Event>,
3940
eventConsumerTransformer: ConsumerTransformer<Event>,
4041
effectHandler: AnyConnectable<Effect, Event>,
4142
effects: [Effect],
@@ -72,12 +73,14 @@ public final class MobiusLoop<Model, Event, Effect>: Disposable {
7273

7374
// These must be set up after consumeEvent, which refers to self; that’s why they need to be IUOs.
7475
self.effectConnection = effectHandler.connect(consumeEvent)
75-
let eventSourceDisposable = eventSource.subscribe(consumer: consumeEvent)
76+
self.eventSourceConnection = eventSource.connect { event in
77+
consumeEvent(event) // TODO what about the model??
78+
}
7679

7780
self.disposable = CompositeDisposable(disposables: [
7881
effectConnection,
7982
modelPublisher,
80-
eventSourceDisposable,
83+
eventSourceConnection,
8184
])
8285

8386
// Prime the modelPublisher, and queue up any initial effects.
@@ -154,6 +157,7 @@ public final class MobiusLoop<Model, Event, Effect>: Disposable {
154157
private func processNext(_ next: Next<Model, Effect>) {
155158
if let newModel = next.model {
156159
model = newModel
160+
eventSourceConnection.accept(model)
157161
modelPublisher.post(model)
158162
}
159163

MobiusCore/Test/MobiusControllerTests.swift

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,11 @@ class MobiusControllerTests: QuickSpec {
2828
override func spec() {
2929
describe("MobiusController") {
3030
var controller: MobiusController<String, String, String>!
31+
var updateFunction: Update<String, String, String>!
32+
var initiate: Initiate<String, String>!
3133
var view: RecordingTestConnectable!
3234
var eventSource: TestEventSource<String>!
35+
var connectableEventSource: TestConnectableEventSource<String, String>!
3336
var effectHandler: RecordingTestConnectable!
3437
var activateInitiator: Bool!
3538

@@ -42,13 +45,13 @@ class MobiusControllerTests: QuickSpec {
4245
view = RecordingTestConnectable(expectedQueue: self.viewQueue)
4346
let loopQueue = self.loopQueue
4447

45-
let updateFunction = Update<String, String, String> { model, event in
48+
updateFunction = .init { model, event in
4649
dispatchPrecondition(condition: .onQueue(loopQueue))
4750
return .next("\(model)-\(event)")
4851
}
4952

5053
activateInitiator = false
51-
let initiate: Initiate<String, String> = { model in
54+
initiate = .init { model in
5255
if activateInitiator {
5356
return First(model: "\(model)-init", effects: ["initEffect"])
5457
} else {
@@ -57,6 +60,7 @@ class MobiusControllerTests: QuickSpec {
5760
}
5861

5962
eventSource = TestEventSource()
63+
6064
effectHandler = RecordingTestConnectable()
6165

6266
controller = Mobius.loop(update: updateFunction, effectHandler: effectHandler)
@@ -356,6 +360,35 @@ class MobiusControllerTests: QuickSpec {
356360
}
357361
}
358362

363+
describe("dispatching events using connectable") {
364+
beforeEach {
365+
// Rebuild the controller but use the Connectable instead of plain EventSource
366+
connectableEventSource = .init()
367+
368+
controller = Mobius.loop(update: updateFunction, effectHandler: effectHandler)
369+
.withEventSource(connectableEventSource)
370+
.makeController(
371+
from: "S",
372+
initiate: initiate,
373+
loopQueue: self.loopQueue,
374+
viewQueue: self.viewQueue
375+
)
376+
controller.connectView(view)
377+
controller.start()
378+
}
379+
380+
it("should dispatch events from the event source") {
381+
connectableEventSource.dispatch("event source event")
382+
383+
expect(view.recorder.items).toEventually(equal(["S", "S-event source event"]))
384+
}
385+
386+
it("should receive models from the event source") {
387+
view.dispatch("new model")
388+
expect(connectableEventSource.models).toEventually(equal(["S", "S-new model"]))
389+
}
390+
}
391+
359392
describe("deallocating") {
360393
var modelObserver: MockConsumerConnectable!
361394
var effectObserver: MockConnectable!

MobiusCore/Test/TestingUtil.swift

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,60 @@ class TestEventSource<Event>: EventSource {
193193
}
194194
}
195195
}
196+
197+
class TestConnectableEventSource<Model, Event>: Connectable {
198+
typealias Input = Model
199+
typealias Output = Event
200+
201+
enum Connection {
202+
case disposed
203+
case active(Consumer<Event>)
204+
}
205+
private(set) var connections: [Connection] = []
206+
private(set) var models: [Model] = []
207+
private var pendingEvent: Event?
208+
209+
var activeConnections: [Consumer<Event>] {
210+
return connections.compactMap {
211+
switch $0 {
212+
case .disposed:
213+
return nil
214+
case .active(let consumer):
215+
return consumer
216+
}
217+
}
218+
}
219+
220+
var allDisposed: Bool {
221+
return activeConnections.isEmpty
222+
}
223+
224+
func connect(_ consumer: @escaping MobiusCore.Consumer<Event>) -> MobiusCore.Connection<Model> {
225+
let index = connections.count
226+
connections.append(.active(consumer))
227+
228+
if let event = pendingEvent {
229+
consumer(event)
230+
pendingEvent = nil
231+
}
232+
233+
return .init(
234+
acceptClosure: { [weak self] model in
235+
self?.models.append(model)
236+
}, disposeClosure: { [weak self] in
237+
self?.connections[index] = .disposed
238+
}
239+
)
240+
}
241+
242+
// Set an event to dispatch immediately when subscribed
243+
func dispatchOnSubscribe(_ event: Event) {
244+
pendingEvent = event
245+
}
246+
247+
func dispatch(_ event: Event) {
248+
activeConnections.forEach {
249+
$0(event)
250+
}
251+
}
252+
}

0 commit comments

Comments
 (0)