Catch circular dependency between observables
Asked Answered
R

1

2

I have a user-programming scenario where user can end up creating two observables that depend on each other. RxJS does not allow circular dependencies, as far as I can see, the memory or stack reaches its limits and the onError callback is triggered with the value true.

How to detect the circular dependency explicitly and throw a more descriptive error message?

This codes illustrates how to create a circular dependency in RxJS:

var obsA,
    obsB;

obsA = Rx.Observable
    .returnValue(42)
    .combineLatest(obsB, function (a, b) {
        return a + b;
    });

obsB = Rx.Observable
    .returnValue(42)
    .combineLatest(obsA, function (b, a) {
        return b + a;
    });


obsA
    .subscribe(function (val) {
        console.log('onNext:' + val);
    },
    function (err) {
        console.error('onError: ' + err);
    },
    function () {
        console.log('onCompleted');
    });

The error message is simply true.

Revolutionist answered 29/6, 2013 at 21:30 Comment(0)
M
2

The code in the original question does not create a circular dependency. At the time you define ObsA, ObsB is undefined and so what you've really done is call combineLatest(undefined, function ...). So the error you are seeing is because you are passing undefined to combinedLatest().

It actually takes some effort to create a real circular dependency. If you use defer, then you would have a true circular dependency:

var obsA,
    obsB,
    aRef,
    bRef;

aRef = Rx.Observable.defer(function () {
    return obsA;
});

bRef = Rx.Observable.defer(function () {
    return obsB;
});

obsA = Rx.Observable
    .returnValue(42)
    .combineLatest(bRef, function (a, b) {
            return a + b;
    });

obsB = Rx.Observable
    .returnValue(42)
    .combineLatest(aRef, function (b, a) {
        return b + a;
    });

obsA.subscribe();
<script src='https://rawgit.com/Reactive-Extensions/RxJS/v.2.5.3/dist/rx.all.js'></script>

Now that is a real circular dependency. Unfortunately you still get the same error, though with a much deeper stack trace:

RangeError: Maximum call stack size exceeded.
/* ... stack ... */

There is no fool-proof way to detect cycles. You could wrap the observables in a new observable and detect recursive calls to your subscribe method. But such an algorithm would be defeated if the underlying observables are using subscribeOn or publish or concat anything else that delays the actual circular subscriptions.

The best suggestion I have is to append a catch clause that checks for a range error and replaces it with a better error:

var obsA,
    obsB,
    aRef,
    bRef;

aRef = Rx.Observable.defer(function () {
    return obsA;
});

bRef = Rx.Observable.defer(function () {
    return obsB;
});

obsA = Rx.Observable
    .returnValue(42)
    .combineLatest(bRef, function (a, b) {
            return a + b;
    })
    .catch(function (e) {
        var isStackError = e instanceof RangeError && e.message === 'Maximum call stack size exceeded';
    
        return Rx.Observable.throw(isStackError ? new Error('Invalid, possibly circular observables.') : e);
    });

obsB = Rx.Observable
    .returnValue(42)
    .combineLatest(aRef, function (b, a) {
        return b + a;
    })
    .catch(function (e) {
        var isStackError = e instanceof RangeError && e.message === 'Maximum call stack size exceeded';
    
        return Rx.Observable.throw(isStackError ? new Error('Invalid, possibly circular observables.') : e);
    });

obsA.subscribe();
<script src='https://rawgit.com/Reactive-Extensions/RxJS/v.2.5.3/dist/rx.all.js'></script>
Madder answered 1/7, 2013 at 2:32 Comment(5)
You are of course right, there was no cyclic dependency. Thanks for the answer!Revolutionist
Here is a outline of a solution I came up with: jsfiddle.net/xGhnC ... it uses the classic spreadsheet algorithm for detecting cyclic dependencies. As far as I can see, it should not cause problems with subscribeOn, publish, or concat. Am I right?Revolutionist
Oh yeah if you can wrap the actual create methods of the observables, then your method will work because you are detecting cyclic dependencies in the "build" step. Here is a minor tweak to ensure your state variables get reset if an error occurs: jsfiddle.net/bman654/xGhnC/2 However, I can still defeat your check by using defer: jsfiddle.net/bman654/xGhnC/3 (notice no error when I call a() to create my observable and then if I subscribe, we get your original problem.Madder
Cool, thanks. Wondering why you add try/finally. If there is a cyclic dependency, I dont need to recover. I am converting from a DSL, so I can prevent stuff like defer.Revolutionist
ah if you do not need to recover then yes you probably do not need the try/finally.Madder

© 2022 - 2024 — McMap. All rights reserved.