Elastic Path Commerce Development

RxJava

RxJava

The Helix programming model for Cortex uses a subset of RxJava classes as expected return types for all prototype operations. This document covers the specific parts of RxJava used by Cortex. More in-depth documentation can be found at reactivex.io.

Prototype Operations and RxJava

All prototype operation interfaces return one of the following RxJava types: Observable<T>, Single<T>, Maybe<T> or Completable.

Observable

Where a prototype class expects zero or more values to be returned, the generated operation interfaces return io.reactivex.Observable.

To return an Observable from a collection:

  Observable.just(someList);
      

In the event of zero values being returned, an empty Observable can be returned:

  Observable.empty();
      

Observable can also return an error:

  Observable.error(new MyException());
      

Single

In cases where a prototype class expects a single return value, the generated operation interfaces return io.reactivex.Single. Single can be used to express that either a value has been emitted or that the operation has failed.

To express a value:

  Single.just("foo");
      

To express a failure:

  Single.error(new MyException());
      

Note that in contrast to Observable<T>, there is no concept of an empty return value. Single must either return a value or an error.

Maybe

In cases where a prototype class expects an optional single return value, the generated operation interfaces return io.reactivex.Maybe. Maybe can be used to express that a value has been emitted, the value is empty, or that the operation has failed.

To express a value:

  Maybe.just("foo");
      

In the event of an empty value, an empty Maybe can be returned:

  Maybe.empty();
      

To express a failure:

  Maybe.error(new MyException());
      

Completable

In cases where a prototype class is not interested in a return value of generated operation interfaces, io.reactivex.Completable is returned. Completable indicates whether an operation was successful or it failed.

To express a successful computation:

  Completable.complete();
      

To express a failure:

  Completable.error(new MyException());
       

Operation Failures

The only runtime exceptions that prototype classes understand are ResourceOperationFailure exceptions. These should be used.

For example, to return a NOT_FOUND failure:

  return Observable.error(ResourceOperationFailure.notFound());
      

Rx Repositories

In some Helix Pattern Library examples, you'll see repositories that return RxJava Observable, Single, Maybe or Completable types. While this is not necessary, returning these allows prototype implementations to make use of the various Rx combinators.

For example:

  @Override
  public Single<ThingEntity> onRead() {
    return repository
      .retrieveThing(thingId.getValue())
      .map(thing -> ThingEntity.builder()
      .withName(thing.getName())
      .withDescription(thing.getDescription())
        .build());
    }
      

In the preceding example, the repository returns a Single<Thing> which allows use of the map operator to convert the returned value into a ThingEntity.

The same example, but with a "regular" return type:

  @Override
  public Single<ThingEntity> onRead() {
    Thing thing = repository.retrieveThing(thingId.getValue());
    ThingEntity thingEntity =  ThingEntity.builder()
      .withName(thing.getName())
      .withDescription(thing.getDescription())
      .build();
    return Single.just(thingEntity);
  }
      

In addition to being able to use Rx combinators, maintaining the "Observable chain" makes handling and propagating errors fairly simple, without need for try/catch blocks.

RxJava Unit Testing

Unit testing of RxJava Observable types is straightforward. The RxJava test() method returns a TestObserver<T> object, which supports a wide range of xunit type test assertions.

Here is an example of testing the Observable<String> getWishlistIds(...) repository method:

  repository.getWishlistIds("customer-id", STORE_CODE)
          .test()
          .assertNoErrors()
          .assertValueCount(1)
          .assertValue(GOOD_WISH_LIST_GUID);
   

Note: The TestObserver can be used with all observable types: Observable, Single, Maybe and Completable.

Debugging via Stack Traces

To debug RxJava code via stack traces, use the RxJava2Extensions plugin, available from RxJava's github.

RxJava code is executed upon subscription to an Observable type. Because of this, when a failure occurs, an exception stack trace may not tell which Rx operation failed. The RxJavaAssemblyTracking class captures the stack trace when Observable, Single, Maybe and Completable operators are instantiated at assembly-time. Whenever an error is signaled via onError(), this assembly-time stack trace is attached as the last cause of that exception.

Example: Enabling Assembly Tracking for the Entire Application

Note:

Enabling assembly tracking has severe performance impact, and it should never be enabled in production. It should only be used for debugging purposes in development.

Add the dependency for RxJava2Extensions:

<dependency>
    <groupId>com.github.akarnokd</groupId>
    <artifactId>rxjava2-extensions</artifactId>
    <version>0.18.1</version>
</dependency>
        

Enable assembly tracking:

@Component
public class RxAssemplyHookInitializer {
  @Activate
  public void initialize() {
    RxJavaAssemblyTracking.enable();
  }
}
        

Once enabled, use the following snippet:

Observable.empty().single()
  .subscribe(System.out::println, Throwable::printStackTrace);
        

Which will result in a stacktrace similar to the one shown below:

java.lang.NoSuchElementException
at rx.internal.operators.OnSubscribeSingle(OnSubscribeSingle.java:57)
...
RxJavaAssemblyException: assembled
at com.example.TrackingExample(TrackingExample:10)