From 3de1cf6c6d618fe398fef834015b54530911704c Mon Sep 17 00:00:00 2001
From: Xiao Gui <xgui3783@gmail.com>
Date: Mon, 1 Aug 2022 12:12:28 +0200
Subject: [PATCH] bugfix: comprehensive migration of BSEndPt to obs

---
 .../sapi/core/sapiParcellation.ts             | 57 +++++++-----
 src/atlasComponents/sapi/core/sapiRegion.ts   | 89 +++++++++++--------
 src/atlasComponents/sapi/core/sapiSpace.ts    | 51 +++++++----
 .../sapi/features/sapiFeature.ts              |  9 +-
 src/atlasComponents/sapi/module.ts            |  9 +-
 src/atlasComponents/sapi/sapi.service.spec.ts | 82 +++++++++--------
 src/atlasComponents/sapi/sapi.service.ts      | 81 ++++++++++-------
 src/atlasComponents/sapi/stories.base.ts      | 48 ++++++----
 .../parcellationVersion.pipe.spec.ts          |  5 +-
 .../layerCtrl.service/layerCtrl.effects.ts    | 13 +--
 10 files changed, 267 insertions(+), 177 deletions(-)

diff --git a/src/atlasComponents/sapi/core/sapiParcellation.ts b/src/atlasComponents/sapi/core/sapiParcellation.ts
index 4767cc289..085c5a82b 100644
--- a/src/atlasComponents/sapi/core/sapiParcellation.ts
+++ b/src/atlasComponents/sapi/core/sapiParcellation.ts
@@ -1,4 +1,5 @@
 import { Observable } from "rxjs"
+import { switchMap } from "rxjs/operators"
 import { SapiVolumeModel } from ".."
 import { SAPI } from "../sapi.service"
 import {SapiParcellationFeatureModel, SapiParcellationModel, SapiQueryPriorityArg, SapiRegionModel} from "../type"
@@ -20,43 +21,53 @@ export class SAPIParcellation{
   }
 
   getDetail(queryParam?: SapiQueryPriorityArg): Observable<SapiParcellationModel>{
-    return this.sapi.httpGet<SapiParcellationModel>(
-      `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}`,
-      null,
-      queryParam
+    return SAPI.BsEndpoint$.pipe(
+      switchMap(endpt => this.sapi.httpGet<SapiParcellationModel>(
+        `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}`,
+        null,
+        queryParam
+      ))
     )
   }
 
   getRegions(spaceId: string, queryParam?: SapiQueryPriorityArg): Observable<SapiRegionModel[]> {
-    return this.sapi.httpGet<SapiRegionModel[]>(
-      `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/regions`,
-      {
-        space_id: spaceId
-      },
-      queryParam
+    return SAPI.BsEndpoint$.pipe(
+      switchMap(endpt => this.sapi.httpGet<SapiRegionModel[]>(
+        `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/regions`,
+        {
+          space_id: spaceId
+        },
+        queryParam
+      ))
     )
   }
   getVolumes(): Observable<SapiVolumeModel[]>{
-    return this.sapi.httpGet<SapiVolumeModel[]>(
-      `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/volumes`
-    )
+    return SAPI.BsEndpoint$.pipe(
+      switchMap(endpt => this.sapi.httpGet<SapiVolumeModel[]>(
+        `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/volumes`
+      ))
+    ) 
   }
 
   getFeatures(parcPagination?: ParcellationPaginationQuery, queryParam?: SapiQueryPriorityArg): Observable<SapiParcellationFeatureModel[]> {
-    return this.sapi.httpGet<SapiParcellationFeatureModel[]>(
-      `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/features`,
-      {
-        type: parcPagination?.type,
-        size: parcPagination?.size?.toString() || '5',
-        page: parcPagination?.page.toString() || '0',
-      },
-      queryParam
+    return SAPI.BsEndpoint$.pipe(
+      switchMap(endpt => this.sapi.httpGet<SapiParcellationFeatureModel[]>(
+        `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/features`,
+        {
+          type: parcPagination?.type,
+          size: parcPagination?.size?.toString() || '5',
+          page: parcPagination?.page.toString() || '0',
+        },
+        queryParam
+      ))
     )
   }
 
   getFeatureInstance(instanceId: string): Observable<SapiParcellationFeatureModel> {
-    return this.sapi.http.get<SapiParcellationFeatureModel>(
-      `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/features/${encodeURIComponent(instanceId)}`,
+    return SAPI.BsEndpoint$.pipe(
+      switchMap(endpt => this.sapi.http.get<SapiParcellationFeatureModel>(
+        `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.id)}/features/${encodeURIComponent(instanceId)}`,
+      ))
     )
   }
 }
diff --git a/src/atlasComponents/sapi/core/sapiRegion.ts b/src/atlasComponents/sapi/core/sapiRegion.ts
index d7b82c7d9..d9263cea0 100644
--- a/src/atlasComponents/sapi/core/sapiRegion.ts
+++ b/src/atlasComponents/sapi/core/sapiRegion.ts
@@ -2,7 +2,7 @@ import { SAPI } from "..";
 import { SapiRegionalFeatureModel, SapiRegionMapInfoModel, SapiRegionModel, cleanIeegSessionDatasets, SapiIeegSessionModel, CleanedIeegDataset, SapiVolumeModel, PaginatedResponse } from "../type";
 import { strToRgb, hexToRgb } from 'common/util'
 import { merge, Observable, of } from "rxjs";
-import { catchError, map, scan } from "rxjs/operators";
+import { catchError, map, scan, switchMap } from "rxjs/operators";
 
 export class SAPIRegion{
 
@@ -16,7 +16,7 @@ export class SAPIRegion{
     return strToRgb(JSON.stringify(region))
   }
 
-  private prefix: string
+  private prefix$: Observable<string>
 
   constructor(
     private sapi: SAPI,
@@ -24,20 +24,26 @@ export class SAPIRegion{
     public parcId: string,
     public id: string,
   ){
-    this.prefix = `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.parcId)}/regions/${encodeURIComponent(this.id)}`
+    this.prefix$ = SAPI.BsEndpoint$.pipe(
+      map(endpt => `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/parcellations/${encodeURIComponent(this.parcId)}/regions/${encodeURIComponent(this.id)}`)
+    )
   }
 
   getFeatures(spaceId: string): Observable<(SapiRegionalFeatureModel | CleanedIeegDataset)[]> {
     return merge(
-      this.sapi.httpGet<SapiRegionalFeatureModel[]>(
-        `${this.prefix}/features`,
-        {
-          space_id: spaceId
-        }
-      ).pipe(
-        catchError((err, obs) => {
-          return of([])
-        })
+      this.prefix$.pipe(
+        switchMap(prefix => 
+          this.sapi.httpGet<SapiRegionalFeatureModel[]>(
+            `${prefix}/features`,
+            {
+              space_id: spaceId
+            }
+          ).pipe(
+            catchError((err, obs) => {
+              return of([])
+            })
+          )
+        )
       ),
       spaceId
         ? this.sapi.getSpace(this.atlasId, spaceId).getFeatures({ parcellationId: this.parcId, region: this.id }).pipe(
@@ -56,50 +62,59 @@ export class SAPIRegion{
   }
 
   getFeatureInstance(instanceId: string, spaceId: string = null): Observable<SapiRegionalFeatureModel> {
-    return this.sapi.httpGet<SapiRegionalFeatureModel>(
-      `${this.prefix}/features/${encodeURIComponent(instanceId)}`,
-      {
-        space_id: spaceId
-      }
+    return this.prefix$.pipe(
+      switchMap(prefix => this.sapi.httpGet<SapiRegionalFeatureModel>(
+        `${prefix}/features/${encodeURIComponent(instanceId)}`,
+        {
+          space_id: spaceId
+        }
+      ))
     )
   }
 
   getMapInfo(spaceId: string): Observable<SapiRegionMapInfoModel> {
-    return this.sapi.http.get<SapiRegionMapInfoModel>(
-      `${this.prefix}/regional_map/info`,
-      {
-        params: {
-          space_id: spaceId
+    return this.prefix$.pipe(
+      switchMap(prefix => this.sapi.http.get<SapiRegionMapInfoModel>(
+        `${prefix}/regional_map/info`,
+        {
+          params: {
+            space_id: spaceId
+          }
         }
-      }
+      ))
     )
   }
 
-  getMapUrl(spaceId: string): string {
-    return `${this.prefix}/regional_map/map?space_id=${encodeURI(spaceId)}`
+  getMapUrl(spaceId: string): Observable<string> {
+    return this.prefix$.pipe(
+      map(prefix => `${prefix}/regional_map/map?space_id=${encodeURI(spaceId)}`)
+    )
   }
 
   getVolumes(): Observable<PaginatedResponse<SapiVolumeModel>>{
-    const url = `${this.prefix}/volumes`
-    return this.sapi.httpGet<PaginatedResponse<SapiVolumeModel>>(
-      url
+    return this.prefix$.pipe(
+      switchMap(prefix => this.sapi.httpGet<PaginatedResponse<SapiVolumeModel>>(
+        `${prefix}/volumes`
+      ))
     )
   }
 
   getVolumeInstance(volumeId: string): Observable<SapiVolumeModel> {
-    const url = `${this.prefix}/volumes/${encodeURIComponent(volumeId)}`
-    return this.sapi.httpGet<SapiVolumeModel>(
-      url
+    return this.prefix$.pipe(
+      switchMap(prefix => this.sapi.httpGet<SapiVolumeModel>(
+        `${prefix}/volumes/${encodeURIComponent(volumeId)}`
+      ))
     )
   }
 
   getDetail(spaceId: string): Observable<SapiRegionModel> {
-    const url = `${this.prefix}`
-    return this.sapi.httpGet<SapiRegionModel>(
-      url,
-      {
-        space_id: spaceId
-      }
+    return this.prefix$.pipe(
+      switchMap(prefix => this.sapi.httpGet<SapiRegionModel>(
+        prefix,
+        {
+          space_id: spaceId
+        }
+      ))
     )
   }
 }
diff --git a/src/atlasComponents/sapi/core/sapiSpace.ts b/src/atlasComponents/sapi/core/sapiSpace.ts
index 3effd6f16..5f61ae6f6 100644
--- a/src/atlasComponents/sapi/core/sapiSpace.ts
+++ b/src/atlasComponents/sapi/core/sapiSpace.ts
@@ -2,6 +2,7 @@ import { Observable } from "rxjs"
 import { SAPI } from '../sapi.service'
 import { camelToSnake } from 'common/util'
 import {SapiQueryPriorityArg, SapiSpaceModel, SapiSpatialFeatureModel, SapiVolumeModel} from "../type"
+import { map, switchMap } from "rxjs/operators"
 
 type FeatureResponse = {
   features: {
@@ -22,13 +23,21 @@ type SpatialFeatureOpts = RegionalSpatialFeatureOpts | BBoxSpatialFEatureOpts
 
 export class SAPISpace{
 
-  constructor(private sapi: SAPI, public atlasId: string, public id: string){}
+  constructor(private sapi: SAPI, public atlasId: string, public id: string){
+    this.prefix$ = SAPI.BsEndpoint$.pipe(
+      map(endpt => `${endpt}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}`)
+    )
+  }
+
+  private prefix$: Observable<string>
 
   getModalities(param?: SapiQueryPriorityArg): Observable<FeatureResponse> {
-    return this.sapi.httpGet<FeatureResponse>(
-      `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}/features`,
-      null,
-      param
+    return this.prefix$.pipe(
+      switchMap(prefix => this.sapi.httpGet<FeatureResponse>(
+        `${prefix}/features`,
+        null,
+        param
+      ))
     )
   }
 
@@ -37,9 +46,11 @@ export class SAPISpace{
     for (const [key, value] of Object.entries(opts)) {
       query[camelToSnake(key)] = value
     }
-    return this.sapi.httpGet<SapiSpatialFeatureModel[]>(
-      `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}/features`,
-      query
+    return this.prefix$.pipe(
+      switchMap(prefix => this.sapi.httpGet<SapiSpatialFeatureModel[]>(
+        `${prefix}/features`,
+        query
+      ))
     )
   }
 
@@ -48,23 +59,29 @@ export class SAPISpace{
     for (const [key, value] of Object.entries(opts)) {
       query[camelToSnake(key)] = value
     }
-    return this.sapi.httpGet<SapiSpatialFeatureModel>(
-      `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}/features/${encodeURIComponent(instanceId)}`,
-      query
+    return this.prefix$.pipe(
+      switchMap(prefix => this.sapi.httpGet<SapiSpatialFeatureModel>(
+        `${prefix}/features/${encodeURIComponent(instanceId)}`,
+        query
+      ))
     )
   }
 
   getDetail(param?: SapiQueryPriorityArg): Observable<SapiSpaceModel>{
-    return this.sapi.httpGet<SapiSpaceModel>(
-      `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}`,
-      null,
-      param
+    return this.prefix$.pipe(
+      switchMap(prefix => this.sapi.httpGet<SapiSpaceModel>(
+        `${prefix}`,
+        null,
+        param
+      ))
     )
   }
 
   getVolumes(): Observable<SapiVolumeModel[]>{
-    return this.sapi.httpGet<SapiVolumeModel[]>(
-      `${this.sapi.bsEndpoint}/atlases/${encodeURIComponent(this.atlasId)}/spaces/${encodeURIComponent(this.id)}/volumes`,
+    return this.prefix$.pipe(
+      switchMap(prefix => this.sapi.httpGet<SapiVolumeModel[]>(
+        `${prefix}/volumes`,
+      ))
     )
   }
 }
diff --git a/src/atlasComponents/sapi/features/sapiFeature.ts b/src/atlasComponents/sapi/features/sapiFeature.ts
index 8290da0ca..f2f341acc 100644
--- a/src/atlasComponents/sapi/features/sapiFeature.ts
+++ b/src/atlasComponents/sapi/features/sapiFeature.ts
@@ -1,3 +1,4 @@
+import { switchMap } from "rxjs/operators";
 import { SAPI } from "../sapi.service";
 import { SapiFeatureModel } from "../type";
 
@@ -6,8 +7,10 @@ export class SAPIFeature {
 
   }
 
-  public detail$ = this.sapi.httpGet<SapiFeatureModel>(
-    `${SAPI.BsEndpoint}/features/${this.id}`,
-    this.opts
+  public detail$ = SAPI.BsEndpoint$.pipe(
+    switchMap(endpt => this.sapi.httpGet<SapiFeatureModel>(
+      `${endpt}/features/${this.id}`,
+      this.opts
+    ))
   )
 }
diff --git a/src/atlasComponents/sapi/module.ts b/src/atlasComponents/sapi/module.ts
index 6d0757bb5..9b9efaf0e 100644
--- a/src/atlasComponents/sapi/module.ts
+++ b/src/atlasComponents/sapi/module.ts
@@ -1,5 +1,4 @@
-import { APP_INITIALIZER, NgModule } from "@angular/core";
-import { SAPI } from "./sapi.service";
+import { NgModule } from "@angular/core";
 import { CommonModule } from "@angular/common";
 import { HttpClientModule, HTTP_INTERCEPTORS } from "@angular/common/http";
 import { PriorityHttpInterceptor } from "src/util/priority";
@@ -16,16 +15,10 @@ import { MatSnackBarModule } from "@angular/material/snack-bar";
   exports: [
   ],
   providers: [
-    SAPI,
     {
       provide: HTTP_INTERCEPTORS,
       useClass: PriorityHttpInterceptor,
       multi: true
-    },
-    {
-      provide: APP_INITIALIZER,
-      useValue: () => SAPI.SetBsEndPoint(),
-      multi: true
     }
   ]
 })
diff --git a/src/atlasComponents/sapi/sapi.service.spec.ts b/src/atlasComponents/sapi/sapi.service.spec.ts
index 0448de0cc..20233eca6 100644
--- a/src/atlasComponents/sapi/sapi.service.spec.ts
+++ b/src/atlasComponents/sapi/sapi.service.spec.ts
@@ -1,10 +1,10 @@
-import { NEVER } from "rxjs"
+import { finalize } from "rxjs/operators"
 import * as env from "src/environments/environment"
 import { SAPI } from "./sapi.service"
 
 describe("> sapi.service.ts", () => {
   describe("> SAPI", () => {
-    describe("#SetBsEndPoint", () => {
+    describe("#BsEndpoint$", () => {
       let fetchSpy: jasmine.Spy
       let environmentSpy: jasmine.Spy
 
@@ -14,16 +14,10 @@ describe("> sapi.service.ts", () => {
       const atlas1 = 'foo'
       const atlas2 = 'bar'
 
-      let originalBsEndpoint: string
-      beforeAll(() => {
-        originalBsEndpoint = SAPI.BsEndpoint
-      })
+      let subscribedVal: string
 
-      afterAll(() => {
-        SAPI.BsEndpoint = originalBsEndpoint
-      })
-      
       beforeEach(() => {
+        SAPI.ClearBsEndPoint()
         fetchSpy = spyOn(window, 'fetch')
         fetchSpy.and.callThrough()
 
@@ -33,43 +27,70 @@ describe("> sapi.service.ts", () => {
         })
       })
 
+
       afterEach(() => {
+        SAPI.ClearBsEndPoint()
         fetchSpy.calls.reset()
         environmentSpy.calls.reset()
+        subscribedVal = null
       })
 
       describe("> first passes", () => {
-        beforeEach(() => {
+        beforeEach(done => {
           const resp = new Response(JSON.stringify([atlas1]), { headers: { 'content-type': 'application/json' }, status: 200 })
-          fetchSpy.and.resolveTo(resp)
+          fetchSpy.and.callFake(async url => {
+            if (url === `${endpt1}/atlases`) {
+              return resp
+            }
+            throw new Error("controlled throw")
+          })
+          SAPI.BsEndpoint$.pipe(
+            finalize(() => done())
+          ).subscribe(val => {
+            subscribedVal = val
+          })
         })
-        it("> should call fetch once", async () => {
-          await SAPI.SetBsEndPoint()
-          expect(fetchSpy).toHaveBeenCalledTimes(1)
-          expect(fetchSpy).toHaveBeenCalledOnceWith(`${endpt1}/atlases`)
+        it("> should call fetch twice", async () => {
+          expect(fetchSpy).toHaveBeenCalledTimes(2)
+          
+          const allArgs = fetchSpy.calls.allArgs()
+          expect(allArgs.length).toEqual(2)
+          expect(allArgs[0]).toEqual([`${endpt1}/atlases`])
+          expect(allArgs[1]).toEqual([`${endpt2}/atlases`])
         })
 
         it("> endpoint should be set", async () => {
-          await SAPI.SetBsEndPoint()
-          expect(SAPI.BsEndpoint).toBe(endpt1)
+          expect(subscribedVal).toBe(endpt1)
+        })
+
+        it("> additional calls should return cached observable", () => {
+
+          expect(fetchSpy).toHaveBeenCalledTimes(2)
+          SAPI.BsEndpoint$.subscribe()
+          SAPI.BsEndpoint$.subscribe()
+
+          expect(fetchSpy).toHaveBeenCalledTimes(2)
         })
       })
 
       describe("> first fails", () => {
-        beforeEach(() => {
-          let counter = 0
-          fetchSpy.and.callFake(async () => {
-            if (counter === 0) {
-              counter ++
+        beforeEach(done => {
+          fetchSpy.and.callFake(async url => {
+            if (url === `${endpt1}/atlases`) {
               throw new Error(`bla`)
             }
             const resp = new Response(JSON.stringify([atlas1]), { headers: { 'content-type': 'application/json' }, status: 200 })
             return resp
           })
+
+          SAPI.BsEndpoint$.pipe(
+            finalize(() => done())
+          ).subscribe(val => {
+            subscribedVal = val
+          })
         })
 
         it("> should call twice", async () => {
-          await SAPI.SetBsEndPoint()
           expect(fetchSpy).toHaveBeenCalledTimes(2)
           expect(fetchSpy.calls.allArgs()).toEqual([
             [`${endpt1}/atlases`],
@@ -78,18 +99,7 @@ describe("> sapi.service.ts", () => {
         })
 
         it('> should set endpt2', async () => {
-          await SAPI.SetBsEndPoint()
-          expect(SAPI.BsEndpoint).toBe(endpt2)
-        })
-
-        it("> instances bsendpoint should be the updated version", async () => {
-          await SAPI.SetBsEndPoint()
-          const mockHttpClient = {
-            get: jasmine.createSpy()
-          }
-          mockHttpClient.get.and.returnValue(NEVER)
-          const sapi = new SAPI(mockHttpClient as any, null, null)
-          expect(sapi.bsEndpoint).toBe(endpt2)
+          expect(subscribedVal).toBe(endpt2)
         })
       })
     })
diff --git a/src/atlasComponents/sapi/sapi.service.ts b/src/atlasComponents/sapi/sapi.service.ts
index b2b4cb332..ef3dc46e1 100644
--- a/src/atlasComponents/sapi/sapi.service.ts
+++ b/src/atlasComponents/sapi/sapi.service.ts
@@ -1,6 +1,6 @@
 import { Injectable } from "@angular/core";
 import { HttpClient } from '@angular/common/http';
-import { filter, map, shareReplay, switchMap, take } from "rxjs/operators";
+import { catchError, filter, map, shareReplay, switchMap, take, tap } from "rxjs/operators";
 import { SAPIAtlas, SAPISpace } from './core'
 import {
   SapiAtlasModel,
@@ -20,7 +20,7 @@ import { MatSnackBar } from "@angular/material/snack-bar";
 import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service";
 import { EnumColorMapName } from "src/util/colorMaps";
 import { PRIORITY_HEADER } from "src/util/priority";
-import { interval, Observable } from "rxjs";
+import { concat, EMPTY, from, merge, Observable, of } from "rxjs";
 import { SAPIFeature } from "./features";
 import { environment } from "src/environments/environment"
 
@@ -29,34 +29,56 @@ export const SIIBRA_API_VERSION = '0.2.2'
 
 type RegistryType = SAPIAtlas | SAPISpace | SAPIParcellation
 
-@Injectable()
+let BS_ENDPOINT_CACHED_VALUE: Observable<string> = null
+
+@Injectable({
+  providedIn: 'root'
+})
 export class SAPI{
 
-  static async SetBsEndPoint() {
-    let idx = 0
-    const siibraApiEndpts = environment.SIIBRA_API_ENDPOINTS.split(',')
-    while (idx < siibraApiEndpts.length) {
-      const url = siibraApiEndpts[idx]
-      try {
-        const resp = await fetch(`${url}/atlases`)
-        const atlases = await resp.json()
-        if (atlases.length > 0) {
-          SAPI.BsEndpoint = url
-          return
-        }
-      } catch (e) {
-        idx ++
-      }
-    }
-    SAPI.ErrorMessage = `It appears all of our mirrors are not working. The viewer may not be working properly...`
+  /**
+   * Used to clear BsEndPoint, so the next static get BsEndpoints$ will
+   * fetch again. Only used for unit test of BsEndpoint$
+   */
+  static ClearBsEndPoint(){
+    BS_ENDPOINT_CACHED_VALUE = null
   }
 
-  static ErrorMessage = null
-  static BsEndpoint = null
-
-  get bsEndpoint() {
-    return SAPI.BsEndpoint
+  /**
+   * BsEndpoint$ is designed as a static getter mainly for unit testing purposes.
+   * see usage of BsEndpoint$ and ClearBsEndPoint in sapi.service.spec.ts
+   */
+  static get BsEndpoint$(): Observable<string> {
+    if (!!BS_ENDPOINT_CACHED_VALUE) return BS_ENDPOINT_CACHED_VALUE
+    BS_ENDPOINT_CACHED_VALUE = concat(
+      merge(
+        ...environment.SIIBRA_API_ENDPOINTS.split(',').map(url => {
+          return from((async () => {
+            const resp = await fetch(`${url}/atlases`)
+            const atlases = await resp.json()
+            if (atlases.length == 0) {
+              throw new Error(`atlas length == 0`)
+            }
+            return url
+          })()).pipe(
+            catchError(() => EMPTY)
+          )
+        })
+      ),
+      of(null).pipe(
+        tap(() => {
+          SAPI.ErrorMessage = `It appears all of our mirrors are not working. The viewer may not be working properly...`
+        }),
+        filter(() => false)
+      )
+    ).pipe(
+      take(1),
+      shareReplay(1),
+    )
+    return BS_ENDPOINT_CACHED_VALUE
   }
+
+  static ErrorMessage = null
   
   registry = {
     _map: {} as Record<string, {
@@ -116,7 +138,9 @@ export class SAPI{
   }
 
   getModalities(): Observable<SapiModalityModel[]> {
-    return this.http.get<SapiModalityModel[]>(`${SAPI.BsEndpoint}/modalities`)
+    return SAPI.BsEndpoint$.pipe(
+      switchMap(endpt => this.http.get<SapiModalityModel[]>(`${endpt}/modalities`))
+    )
   }
 
   httpGet<T>(url: string, params?: Record<string, string>, sapiParam?: SapiQueryPriorityArg){
@@ -133,10 +157,7 @@ export class SAPI{
     )
   }
 
-  public atlases$ = interval(160).pipe(
-    map(() => this.bsEndpoint),
-    filter(v => !!v),
-    take(1),
+  public atlases$ = SAPI.BsEndpoint$.pipe(
     switchMap(endpt => this.http.get<SapiAtlasModel[]>(
       `${endpt}/atlases`,
       {
diff --git a/src/atlasComponents/sapi/stories.base.ts b/src/atlasComponents/sapi/stories.base.ts
index 1fa58bef8..57e28f20d 100644
--- a/src/atlasComponents/sapi/stories.base.ts
+++ b/src/atlasComponents/sapi/stories.base.ts
@@ -67,22 +67,27 @@ export const parcId = {
 }
 
 export async function getAtlases(): Promise<SapiAtlasModel[]> {
-  return await (await fetch(`${SAPI.BsEndpoint}/atlases`)).json() as SapiAtlasModel[]
+  const endPt = await SAPI.BsEndpoint$.toPromise()
+  return await (await fetch(`${endPt}/atlases`)).json() as SapiAtlasModel[]
 }
 
 export async function getAtlas(id: string): Promise<SapiAtlasModel>{
-  return await (await fetch(`${SAPI.BsEndpoint}/atlases/${id}`)).json()
+  const endPt = await SAPI.BsEndpoint$.toPromise()
+  return await (await fetch(`${endPt}/atlases/${id}`)).json()
 }
 
 export async function getParc(atlasId: string, id: string): Promise<SapiParcellationModel>{
-  return await (await fetch(`${SAPI.BsEndpoint}/atlases/${atlasId}/parcellations/${id}`)).json()
+  const endPt = await SAPI.BsEndpoint$.toPromise()
+  return await (await fetch(`${endPt}/atlases/${atlasId}/parcellations/${id}`)).json()
 }
 export async function getParcRegions(atlasId: string, id: string, spaceId: string): Promise<SapiRegionModel[]>{
-  return await (await fetch(`${SAPI.BsEndpoint}/atlases/${atlasId}/parcellations/${id}/regions?space_id=${encodeURIComponent(spaceId)}`)).json()
+  const endPt = await SAPI.BsEndpoint$.toPromise()
+  return await (await fetch(`${endPt}/atlases/${atlasId}/parcellations/${id}/regions?space_id=${encodeURIComponent(spaceId)}`)).json()
 }
 
 export async function getSpace(atlasId: string, id: string): Promise<SapiSpaceModel> {
-  return await (await fetch(`${SAPI.BsEndpoint}/atlases/${atlasId}/spaces/${id}`)).json()
+  const endPt = await SAPI.BsEndpoint$.toPromise()
+  return await (await fetch(`${endPt}/atlases/${atlasId}/spaces/${id}`)).json()
 }
 
 export async function getHumanAtlas(): Promise<SapiAtlasModel> {
@@ -90,7 +95,8 @@ export async function getHumanAtlas(): Promise<SapiAtlasModel> {
 }
 
 export async function getMni152(): Promise<SapiSpaceModel> {
-  return await (await fetch(`${SAPI.BsEndpoint}/atlases/${atlasId.human}/spaces/${spaceId.human.mni152}`)).json()
+  const endPt = await SAPI.BsEndpoint$.toPromise()
+  return await (await fetch(`${endPt}/atlases/${atlasId.human}/spaces/${spaceId.human.mni152}`)).json()
 }
 
 export async function getJba29(): Promise<SapiParcellationModel> {
@@ -103,33 +109,41 @@ export async function getJba29Regions(): Promise<SapiRegionModel[]> {
 
 export async function getHoc1Right(spaceId=null): Promise<SapiRegionModel> {
   if (!spaceId) {
-    return await (await fetch(`${SAPI.BsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right`)).json()
+    const endPt = await SAPI.BsEndpoint$.toPromise()
+    return await (await fetch(`${endPt}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right`)).json()
   }
-  return await (await fetch(`${SAPI.BsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right?space_id=${encodeURIComponent(spaceId)}`)).json()
+  const endPt = await SAPI.BsEndpoint$.toPromise()
+  return await (await fetch(`${endPt}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right?space_id=${encodeURIComponent(spaceId)}`)).json()
 }
 
 export async function get44Left(spaceId=null): Promise<SapiRegionModel> {
   if (!spaceId) {
-    return await (await fetch(`${SAPI.BsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/area%2044%20left`)).json()
+    const endPt = await SAPI.BsEndpoint$.toPromise()
+    return await (await fetch(`${endPt}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/area%2044%20left`)).json()
   }
-  return await (await fetch(`${SAPI.BsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/area%2044%20left?space_id=${encodeURIComponent(spaceId)}`)).json()
+  const endPt = await SAPI.BsEndpoint$.toPromise()
+  return await (await fetch(`${endPt}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/area%2044%20left?space_id=${encodeURIComponent(spaceId)}`)).json()
 }
 
 export async function getHoc1RightSpatialFeatures(): Promise<SxplrCleanedFeatureModel[]> {
-  const json: SapiSpatialFeatureModel[] = await (await fetch(`${SAPI.BsEndpoint}/atlases/${atlasId.human}/spaces/${spaceId.human.mni152}/features?parcellation_id=2.9&region=hoc1%20right`)).json()
+  const endPt = await SAPI.BsEndpoint$.toPromise()
+  const json: SapiSpatialFeatureModel[] = await (await fetch(`${endPt}/atlases/${atlasId.human}/spaces/${spaceId.human.mni152}/features?parcellation_id=2.9&region=hoc1%20right`)).json()
   return cleanIeegSessionDatasets(json.filter(it => it['@type'] === "siibra/features/ieegSession"))
 }
 
 export async function getHoc1RightFeatures(): Promise<SapiRegionalFeatureModel[]> {
-  return await (await fetch(`${SAPI.BsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right/features`)).json()
+  const endPt = await SAPI.BsEndpoint$.toPromise()
+  return await (await fetch(`${endPt}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right/features`)).json()
 }
 
 export async function getHoc1RightFeatureDetail(featId: string): Promise<SapiRegionalFeatureModel>{
-  return await (await fetch(`${SAPI.BsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right/features/${encodeURIComponent(featId)}`)).json()
+  const endPt = await SAPI.BsEndpoint$.toPromise()
+  return await (await fetch(`${endPt}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/regions/hoc1%20right/features/${encodeURIComponent(featId)}`)).json()
 }
 
 export async function getJba29Features(): Promise<SapiParcellationFeatureModel[]> {
-  return await (await fetch(`${SAPI.BsEndpoint}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/features`)).json()
+  const endPt = await SAPI.BsEndpoint$.toPromise()
+  return await (await fetch(`${endPt}/atlases/${atlasId.human}/parcellations/${parcId.human.jba29}/features`)).json()
 }
 
 export async function getBigbrainSpatialFeatures(): Promise<SapiSpatialFeatureModel[]>{
@@ -137,14 +151,16 @@ export async function getBigbrainSpatialFeatures(): Promise<SapiSpatialFeatureMo
     [-1000, -1000, -1000],
     [1000, 1000, 1000]
   ]
-  const url = new URL(`${SAPI.BsEndpoint}/atlases/${atlasId.human}/spaces/${spaceId.human.bigbrain}/features`)
+  const endPt = await SAPI.BsEndpoint$.toPromise()
+  const url = new URL(`${endPt}/atlases/${atlasId.human}/spaces/${spaceId.human.bigbrain}/features`)
   url.searchParams.set(`bbox`, JSON.stringify(bbox))
   return await (await fetch(url.toString())).json()
 }
 
 export async function getMni152SpatialFeatureHoc1Right(): Promise<SapiSpatialFeatureModel[]>{
   
-  const url = new URL(`${SAPI.BsEndpoint}/atlases/${atlasId.human}/spaces/${spaceId.human.mni152}/features`)
+  const endPt = await SAPI.BsEndpoint$.toPromise()
+  const url = new URL(`${endPt}/atlases/${atlasId.human}/spaces/${spaceId.human.mni152}/features`)
   url.searchParams.set(`parcellation_id`, parcId.human.jba29)
   url.searchParams.set("region", 'hoc1 right')
   return await (await fetch(url.toString())).json()
diff --git a/src/atlasComponents/sapiViews/core/parcellation/parcellationVersion.pipe.spec.ts b/src/atlasComponents/sapiViews/core/parcellation/parcellationVersion.pipe.spec.ts
index 972e41179..ed2edbf53 100644
--- a/src/atlasComponents/sapiViews/core/parcellation/parcellationVersion.pipe.spec.ts
+++ b/src/atlasComponents/sapiViews/core/parcellation/parcellationVersion.pipe.spec.ts
@@ -3,11 +3,12 @@ import { SAPI } from "src/atlasComponents/sapi/sapi.service"
 import { SapiParcellationModel } from "src/atlasComponents/sapi/type"
 import { getTraverseFunctions } from "./parcellationVersion.pipe"
 
-describe(`parcellationVersion.pipe.ts (endpoint at ${SAPI.BsEndpoint})`, () => {
+describe(`parcellationVersion.pipe.ts`, () => {
   describe("getTraverseFunctions", () => {
     let julichBrainParcellations: SapiParcellationModel[] = []
     beforeAll(async () => {
-      const res = await fetch(`${SAPI.BsEndpoint}/atlases/${encodeURIComponent(IDS.ATLAES.HUMAN)}/parcellations`)
+      const bsEndPoint = await SAPI.BsEndpoint$.toPromise()
+      const res = await fetch(`${bsEndPoint}/atlases/${encodeURIComponent(IDS.ATLAES.HUMAN)}/parcellations`)
       const arr: SapiParcellationModel[] = await res.json()
       julichBrainParcellations = arr.filter(it => /Julich-Brain Cytoarchitectonic Maps/.test(it.name))
     })
diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts
index 579f7fb79..6886634b6 100644
--- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts
+++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.effects.ts
@@ -37,17 +37,20 @@ export class LayerCtrlEffects {
     ),
     switchMap(([ regions, { atlas, parcellation, template } ]) => {
       const sapiRegion = this.sapi.getRegion(atlas["@id"], parcellation["@id"], regions[0].name)
-      return sapiRegion.getMapInfo(template["@id"]).pipe(
-        map(val => 
+      return forkJoin([
+        sapiRegion.getMapInfo(template["@id"]),
+        sapiRegion.getMapUrl(template["@id"])
+      ]).pipe(
+        map(([mapInfo, mapUrl]) => 
           atlasAppearance.actions.addCustomLayer({
             customLayer: {
               clType: "customlayer/nglayer",
               id: PMAP_LAYER_NAME,
-              source: `nifti://${sapiRegion.getMapUrl(template["@id"])}`,
+              source: `nifti://${mapUrl}`,
               shader: getShader({
                 colormap: EnumColorMapName.VIRIDIS,
-                highThreshold: val.max,
-                lowThreshold: val.min,
+                highThreshold: mapInfo.max,
+                lowThreshold: mapInfo.min,
                 removeBg: true,
               })
             }
-- 
GitLab