Repeatedly emitting values from a growing array

Viewed 85

Quick context: I'm displaying a "live" image slideshow, meaning as images are being displayed, users can upload images & those new images should display (eventually). This is "infinite", meaning I want to restart at the first image once all images have been emitted. New incoming images should go to the end of the queue & emit once I loop back to them.

I've come up with what I think is a dirty & incorrect solution that boils down to:

const srcArray = [-2, -1];
const userImageUpload$ = interval(2000).pipe(
  tap((val) => srcArray.push(val))
).subscribe();

const liveImages$ = zip(interval(1000), from(srcArray)).pipe(
  map(([a, b]) => b),
  repeat()
).subscribe((val) => console.log("Image " + val));

Here we have a starting array of some "images". The userImageUpload$ represents the idea of new images incoming. When I get one I push it into the starting array. liveImages$ represents the values I want emitted. This technically works, but altering this shared global array feels bad and may lead to issues. Is there a good solution to do this all in one, possibly with the use of subjects? I'm having trouble coming up with a solution.

3 Answers

I would keep a currentImages$ observable which accumulates all the uploaded images, and a currentImageIndex$ observable which keeps the index of the image which should be currently displayed. Then the liveImages Observable (which combines the two) is the result you want.

const currentImages$ = userImageUpload$.pipe(
    scan((acc, curr) => [...acc, curr], [])
);

const currentImageIndex$ = interval(SLIDE_SHOW_INTERVAL).pipe(
    withLatestFrom(currentImages$),
    map(([, currImages]) => currImages),
    scan((accIndex, currImagesArr) => (accIndex + 1) % currImagesArr.length, 0)
);

const liveImages$ = currentImageIndex$.pipe(
    withLatestFrom(currentImages$),
    map(([currIndex, currImages]) => currImages[currIndex])
);

Use BehaviorSubject makes more sense. I think your solution is fine, the difference in this answer is you get notified when the image get uploaded i.e array get updated, by start off the stream with imageStore.

const imageStore=new BehaviorSubject([-2, -1]);

const userImageUpload$ = interval(2000).pipe(
  map((val) => imageStore.getValue().concat(val))
).subscribe(imageStore);

imageStore.pipe(switchMap(srcArray=>
 zip(interval(1000), from(srcArray)).pipe(
   map(([a, b]) => b),
   repeat()
  )
))

You could try it like this, you can see the same in my stackblitz (Angular example) https://stackblitz.com/edit/angular-ivy-dhvt8r?file=src/app/app.component.html

private images: BehaviorSubject<number[]> = new BehaviorSubject<number[]>([
    -2,
    -1
  ]);
  private imagesChanged$ = this.images.asObservable();

  imageToDisplay$: Observable<number>;

  ngOnInit() {
    this.imageToDisplay$ = this.imagesChanged$.pipe(
      switchMap(() => interval(1000)),
      map((v: number) => v % this.images.value.length),
      map((v: number) => this.images.value[v])
    );
  }

  addImage() {
    this.images.next([
      ...this.images.value,
      this.images.value[this.images.value.length - 1] + 1
    ]);
  }
<div>
    Current Image: {{ imageToDisplay$ | async }}
</div>

<button (click)="addImage()">Add a Image</button>
Related