The general design problem can be described as:
I have a websocket connection that has a strict lifecycle to respect—it wants connect and disconnect to be called appropriately, and, because it talks to the system, it uses . Within this websocket connection, we have multiple different Subscription objects, each with a strict lifecycle that it wants to be respected (subscribe and unsubscribe), and it depends on the state of its parent websocket for those operations to be successful.
Here's a timeline of the ideal behavior for three nested lifecycle observables, where C depends on B which depends on A:
A = someInput.switchMap((i) => LifecycleObservable())
B = A.switchMap((a) => LifecycleObservable())
C = B.switchMap((b) => LifecycleObservable())
C.listen(print);
// <-- listen to c
// <-- produce [someInput]
setup A
setup B
setup C
// <-- c is produced
// <-- c is unsubscribed
teardown C
teardown B
teardown A
// <-- C is re-subscribed-to
setup A
setup B
setup C
// <-- produce [someInput]
teardown C
teardown B
teardown A
setup A
setup B
setup C
// <-- c is produced
First question: Is this an anti-pattern? I haven't been able to find much about this pattern on the web, but it seems like a pretty standard sort of thing you'd run into with observables: some objects just have a lifecycle and some objects might want to depend on that.
I can get pretty close to this ideal behavior using something like this:
class LifecycleObservable {
static Observable<T> fromObservable<T>({
@required Observable<T> input,
@required Future<void> Function(T) setup,
@required Future<void> Function(T) teardown,
}) {
return input.asyncMap((T _input) async {
await setup(_input);
return _input;
}).switchMap((T _input) {
return Observable<T>(Observable.never()) //
.startWith(_input)
.doOnCancel(() async {
await teardown(_input);
});
});
}
}
This code accepts a stream of stateful objects, running setup on them as they're produced and teardown on them as the sub-observable within the switchMap is cancelled.
The problem occurs when, in the original idealized timeline, the second [someInput] is produced: using the code above I get a callgraph like
// <-- listen to c
// <-- produce [someInput]
setup A
setup B
setup C
// <-- c is produced
// <-- produce [someInput]
teardown A
setup A
teardown B
setup B
teardown C
setup C
// <-- c is produced
the problem being that if B depends on A (like calling unsubscribe from a subscription that depends on an open websocket transport), this teardown order breaks the expected lifecycle of each object (the subscription tries to send unsubscribe over a closed websocket transport.