Search code examples
rxjsreactivex

Convert Dynamic Array of Dynamic Arrays to an Observable


I have a dynamic array structure. Specifically, it is Google Maps' MVCArray. This structure has regular put, get, remove methods, as well as an addListener to listen to any changes. A library method (Polygon#getPaths) returns an MVCArray of MVCArray of LatLngs, since a polygon can have any number of paths, and each path can have any number of vertices.

My goal is to convert generate an Observable of PolygonPathEvent which will fire when either the parent MVCArray any child MVCArray will be changed.

Creating the Observables

First order of business is to convert the addListener to Observables.

private createMVCEventObservable<T>(array: MVCArray<T>): Observable<[T[], string, number, T?]>{
    const eventNames = ['insert_at', 'remove_at', 'set_at'];
    return fromEventPattern(
      (handler: Function) => eventNames.map(evName => array.addListener(evName,
        (index: number, previous?: LatLng) => this._zone.run(() => handler.apply(array, [[array.getArray(), evName, index, previous]])))),
      (handler: Function, evListeners: MapsEventListener[]) => evListeners.forEach(evListener => evListener.remove()));
  }

Combining the Observables

Now to combine, seemingly we need to use combineLatest

const pathsChanges$ = this.createMVCEventObservable(paths);
const pathChanges$ = combineLatest(paths.getArray().map(this.createMVCEventObservable));
return combineLatest(pathChanges$, pathsChanges$, (pathArr, paths) => 
  new PolygonPathEvent(pathArr, paths);
);

The problem is that the parent MVCArray of MVCArray can change, but combineLatest takes in a static array. So when there is a new path added, I don't know how to make the returned Observable also listen to this new path. Same, if a path is deleted, I don't know how to make the returned observable unsubscribe from the deleted path.

Piping to Subject (Wrong approach)

I thought about returning a Subject, and simply subscribing it to different observables whenever the parent MVCArray<MVCArray<LatLng>> changes.

const retVal: Subject<PolygonPathEvent> = new Subject();
const pathsChanges$ = this.createMVCEventObservable(paths);
const pathChanges$ = combineLatest(paths.getArray().map(this.createMVCEventObservable));
let latestSubscription = combineLatest(pathChanges$, pathsChanges$, (pathArr, paths) => 
  new PolygonPathEvent(pathArr, paths)
).subscribe(retVal);

pathsChanges$.pipe(tap( ([arrays, event, index, previous]) => {
  latestSubscription.unsubscribe();
  latestSubscription = combineLatest(pathChanges$, pathsChanges$, (pathArr, paths) => 
  new PolygonPathEvent(pathArr, paths)
  ).subscribe(retVal);
} ));

return retVal;

This works, the problem is that a subscription to the original Observables (and addListener) happens right in this method, and not when the returned Observable is subscribed to.

Conclusion

I need some kind of operator for this.


Solution

  • If I understand your problem right, there may be space to use switchMap to solve it.

    You say that "The problem is that the parent MVCArray of MVCArray can change, but combineLatest takes in a static array". I think that the static array you refer to is the one emitted by the pathChanges$ Observable, which is created once for all at the beginning of your code snippet and gets not updated when the parent MVCArray of MVCArray changes.

    If my understanding is right, what we need to do is to provide a way to notify the event of change of the parent MVCArray of MVCArray and, any time such event occurs, execute again the combineLatest function using the new updated array.

    This can be accomplished with a logic similar to the following

    const pathsChanges$ = this.createMVCEventObservable(paths);
    
    pathsChanges$
    .pipe(
       switchMap(() => combineLatest(paths.getArray().map(this.createMVCEventObservable))),
       map(pathArr => new PolygonPathEvent(pathArr, paths))
    )
    

    This logic assumes that the variable paths is passed into this piece of logic from the outside.

    I could not reproduce the case and therefore I am far from sure that my answer solves your problem, even if I hope it helps.