diff --git a/docs/releases/v2.10.3.md b/docs/releases/v2.10.3.md
new file mode 100644
index 0000000000000000000000000000000000000000..29a5b69956e8d4d5cd3ce0ea05af33ea733a3b66
--- /dev/null
+++ b/docs/releases/v2.10.3.md
@@ -0,0 +1,5 @@
+# v2.10.3
+
+## Bugfix
+
+- restore the wireframe for VOI (enable via toggle)
diff --git a/mkdocs.yml b/mkdocs.yml
index 0049347033a0f60336c29db577942fecd6ebacfd..1d4cdfe2e840e41a1830c4fb708f8f45518c68af 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -33,6 +33,7 @@ nav:
     - Fetching datasets: 'advanced/datasets.md'
     - Display non-atlas volumes: 'advanced/otherVolumes.md'
   - Release notes:
+    - v2.10.3: 'releases/v2.10.3.md'
     - v2.10.2: 'releases/v2.10.2.md'
     - v2.10.1: 'releases/v2.10.1.md'
     - v2.10.0: 'releases/v2.10.0.md'
diff --git a/package.json b/package.json
index 34add91c280786f89455ea5df1c314d5fe263df3..e38a7b798ce911e4604c3b8d7a6fdb35c4a44514 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "siibra-explorer",
-  "version": "2.10.2",
+  "version": "2.10.3",
   "description": "siibra-explorer - explore brain atlases. Based on humanbrainproject/nehuba & google/neuroglancer. Built with angular",
   "scripts": {
     "lint": "eslint src --ext .ts",
diff --git a/src/features/category-acc.directive.ts b/src/features/category-acc.directive.ts
index ca2b5e818862b99fab0e3f8f8a513c75356b34f0..ea8afb4f1a365beb1bffbd41a015ab7d28caf8e1 100644
--- a/src/features/category-acc.directive.ts
+++ b/src/features/category-acc.directive.ts
@@ -93,6 +93,7 @@ export class CategoryAccDirective implements AfterContentInit, OnDestroy {
     this.#unchecked$.next(val)
   }
 
+  datasource: ParentDatasource<TranslatedFeature>
   public datasource$ = combineLatest([
     this.unchecked$,
     this.#listCmps$,
@@ -111,11 +112,46 @@ export class CategoryAccDirective implements AfterContentInit, OnDestroy {
       return combineLatest(
         filteredListCmps.map(cmp => cmp.datasource$)
       ).pipe(
-        map(dss => new ParentDatasource({ children: dss })),
+        map(dss => {
+          this.datasource = new ParentDatasource({ children: dss })
+          return this.datasource
+        }),
       )
     })
   )
 
+  constructor(){
+
+    /**
+     * On init, if current count is less than 50, and less than total, pull.
+     */
+    this.#subscriptions.push(
+      combineLatest([
+        this.total$,
+        this.datasource$
+      ]).pipe(
+        switchMap(([ total, ds ]) => 
+          ds.data$.pipe(
+            map(items => ({
+              total,
+              ds,
+              current: items.length
+            }))
+          )
+        )
+      ).subscribe(async ({ total, current, ds }) => {
+        if (total > current && current < 50) {
+          try {
+            await ds.pull()
+          } catch (e) {
+            // if already pulling, ignore
+          }
+        }
+      })
+    )
+  }
+
+  #subscriptions: Subscription[] = []
   #changeSub: Subscription
   ngAfterContentInit(): void {
     this.#changeSub = this.listCmps.changes.subscribe(() => {
@@ -130,5 +166,6 @@ export class CategoryAccDirective implements AfterContentInit, OnDestroy {
 
   ngOnDestroy(): void {
     if (this.#changeSub) this.#changeSub.unsubscribe() 
+    if (this.#subscriptions.length > 0) this.#subscriptions.pop().unsubscribe()
   }
 }
diff --git a/src/features/entry/entry.component.html b/src/features/entry/entry.component.html
deleted file mode 100644
index ad474847c41c6ccd4a3c7d3c390b2855f9f01152..0000000000000000000000000000000000000000
--- a/src/features/entry/entry.component.html
+++ /dev/null
@@ -1,91 +0,0 @@
-<mat-accordion>
-    <mat-expansion-panel *ngFor="let keyvalue of (cateogryCollections$ | async | keyvalue | isConnectivity : false)"
-        sxplrCategoryAcc
-        #categoryAcc="categoryAcc"
-        [ngClass]="{
-            'sxplr-d-none': !(categoryAcc.isBusy$ | async) && (categoryAcc.total$ | async) === 0
-        }">
-
-        <mat-expansion-panel-header>
-
-            <mat-panel-title>
-                {{ keyvalue.key }}
-            </mat-panel-title>
-            
-            <mat-panel-description>
-                <spinner-cmp *ngIf="categoryAcc.isBusy$ | async"></spinner-cmp>
-                <ng-template [ngIf]="categoryAcc.total$ | async" let-total>
-                    <span>
-                        {{ total }}
-                    </span>
-                </ng-template>
-            </mat-panel-description>
-        </mat-expansion-panel-header>
-
-        <div class="c3-outer">
-            <div class="c3-inner">
-                
-                <mat-card class="c3 mat-elevation-z4"
-                    *ngFor="let feature of keyvalue.value"
-                    [ngClass]="{
-                        'sxplr-d-none': (list.state$ | async) === 'noresult'
-                    }">
-                    <mat-card-header>
-
-                        <mat-card-title>
-                            <span class="category-title sxplr-white-space-nowrap">
-                                {{ feature.name | featureNamePipe }}
-                            </span>
-                        </mat-card-title>
-                        
-                    </mat-card-header>
-
-
-                    <mat-card-content>
-                        <spinner-cmp *ngIf="(list.state$ | async) === 'busy'"></spinner-cmp>
-                        <sxplr-feature-list
-                            [template]="template"
-                            [parcellation]="parcellation"
-                            [region]="region"
-                            [bbox]="bbox"
-                            [queryParams]="queryParams | mergeObj : { type: (feature.name | featureNamePipe) }"
-                            [featureRoute]="feature.path"
-                            (onClickFeature)="onClickFeature($event)"
-                            #list="featureList"
-                            >
-                        </sxplr-feature-list>
-
-                        
-                    </mat-card-content>
-                </mat-card>
-            </div>
-        </div>
-    </mat-expansion-panel>
-    
-
-    <ng-template [ngIf]="cateogryCollections$ | async | keyvalue | isConnectivity : true" let-connectivity>
-        <ng-template ngFor [ngForOf]="connectivity" let-conn>
-            <mat-expansion-panel sxplr-sapiviews-features-connectivity-check
-                #connectivityAccordion
-                *ngIf="conn">
-                <mat-expansion-panel-header>
-                    <mat-panel-title>
-                        {{ conn.key }}
-                    </mat-panel-title>
-                </mat-expansion-panel-header>
-
-                <sxplr-features-connectivity-browser class="pe-all flex-shrink-1" 
-                    [region]="region"
-                    [sxplr-features-connectivity-browser-atlas]="atlas | async"
-                    [sxplr-features-connectivity-browser-template]="template"
-                    [sxplr-features-connectivity-browser-parcellation]="parcellation"
-                    [accordionExpanded]="connectivityAccordion.expanded"
-                    [types]="conn.value">
-                </sxplr-features-connectivity-browser>
-
-            </mat-expansion-panel>
-        </ng-template>
-    </ng-template>
-
-
-</mat-accordion>
diff --git a/src/features/entry/entry.component.scss b/src/features/entry/entry.component.scss
deleted file mode 100644
index 666bfbb6d1f2abfabfb44a4d6fc2095f15693295..0000000000000000000000000000000000000000
--- a/src/features/entry/entry.component.scss
+++ /dev/null
@@ -1,56 +0,0 @@
-mat-list-item
-{
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-
-// card in card container
-.c3-outer
-{
-  display: inline-block;
-  overflow-x: auto;
-  height: 15rem;
-  width: 100%;
-}
-
-.c3-inner
-{
-
-  height: 100%;
-  display: inline-flex;
-  gap: 1.5rem;
-  flex-wrap: nowrap;
-  
-  margin: 0 2rem;
-  align-items: stretch;
-}
-
-.c3-inner > mat-card
-{
-  width: 16rem;
-  overflow:hidden;
-  height: 95%;
-}
-
-.category-title:hover
-{
-  cursor: default;
-}
-
-mat-card.c3
-{
-  display: flex;
-  flex-direction: column;
-}
-
-mat-card.c3 > mat-card-header
-{
-  flex: 0 0 auto;
-}
-
-mat-card.c3 > mat-card-content
-{
-  flex: 1 1 auto;
-  height: 75%;
-  overflow: auto;
-}
\ No newline at end of file
diff --git a/src/features/entry/entry.component.ts b/src/features/entry/entry.component.ts
index abe5920301de67270962a927372ba8efd4a573fe..465e258effca5118cec36f11cb4f52f70bbe246c 100644
--- a/src/features/entry/entry.component.ts
+++ b/src/features/entry/entry.component.ts
@@ -1,6 +1,6 @@
 import { AfterViewInit, Component, OnDestroy, QueryList, ViewChildren } from '@angular/core';
 import { select, Store } from '@ngrx/store';
-import { map, scan, switchMap, tap } from 'rxjs/operators';
+import { map, scan, shareReplay, switchMap, tap } from 'rxjs/operators';
 import { IDS, SAPI } from 'src/atlasComponents/sapi';
 import { Feature } from 'src/atlasComponents/sapi/sxplrTypes';
 import { FeatureBase } from '../base';
@@ -8,7 +8,8 @@ import * as userInteraction from "src/state/userInteraction"
 import { atlasSelection } from 'src/state';
 import { CategoryAccDirective } from "../category-acc.directive"
 import { BehaviorSubject, combineLatest, merge, of, Subscription } from 'rxjs';
-import { IsAlreadyPulling, PulledDataSource } from 'src/util/pullable';
+import { DsExhausted, IsAlreadyPulling, PulledDataSource } from 'src/util/pullable';
+import { TranslatedFeature } from '../list/list.directive';
 
 const categoryAcc = <T extends Record<string, unknown>>(categories: T[]) => {
   const returnVal: Record<string, T[]> = {}
@@ -32,6 +33,11 @@ const categoryAcc = <T extends Record<string, unknown>>(categories: T[]) => {
 })
 export class EntryComponent extends FeatureBase implements AfterViewInit, OnDestroy {
 
+  private _features$ = new BehaviorSubject<TranslatedFeature[]>([])
+  features$ = this._features$.pipe(
+    shareReplay(1)
+  )
+
   @ViewChildren(CategoryAccDirective)
   catAccDirs: QueryList<CategoryAccDirective>
 
@@ -139,11 +145,35 @@ export class EntryComponent extends FeatureBase implements AfterViewInit, OnDest
       try {
         await datasource.pull()
       } catch (e) {
-        if (e instanceof IsAlreadyPulling) {
+        if (e instanceof IsAlreadyPulling || e instanceof DsExhausted) {
           return
         }
         throw e
       }
     }
   }
+
+  async pullAll(){
+    const dss = Array.from(this.catAccDirs).map(catAcc => catAcc.datasource)
+
+    this._features$.next([])
+    await Promise.all(
+      dss.map(async ds => {
+        while (true) {
+          try {
+            await ds.pull()
+          } catch (e) {
+            if (e instanceof DsExhausted) {
+              break
+            }
+            if (e instanceof IsAlreadyPulling ) {
+              continue
+            }
+            throw e
+          }
+        }
+      })
+    )
+    this._features$.next(dss.flatMap(ds => ds.finalValue))
+  }
 }
diff --git a/src/features/entry/entry.nestedExpPanel.component.html b/src/features/entry/entry.nestedExpPanel.component.html
deleted file mode 100644
index 9c82b50fec7c89559079dc19fa05006547af760a..0000000000000000000000000000000000000000
--- a/src/features/entry/entry.nestedExpPanel.component.html
+++ /dev/null
@@ -1,144 +0,0 @@
-<mat-accordion>
-    <mat-expansion-panel *ngFor="let keyvalue of (cateogryCollections$ | async | keyvalue | filterCategory : ['connectivity', 'dataset', 'other'] : false)"
-        sxplrCategoryAcc
-        #categoryAcc="categoryAcc"
-        [ngClass]="{
-            'sxplr-d-none': !(categoryAcc.isBusy$ | async) && (categoryAcc.total$ | async) === 0
-        }">
-
-        <mat-expansion-panel-header>
-
-            <mat-panel-title>
-                {{ keyvalue.key }}
-            </mat-panel-title>
-            
-            <mat-panel-description>
-                <spinner-cmp *ngIf="categoryAcc.isBusy$ | async"></spinner-cmp>
-                <ng-template [ngIf]="categoryAcc.total$ | async" let-total>
-                    <span>
-                        {{ total }}
-                    </span>
-                </ng-template>
-            </mat-panel-description>
-        </mat-expansion-panel-header>
-
-        <mat-accordion>
-            <mat-expansion-panel class="mat-elevation-z4"
-                *ngFor="let feature of keyvalue.value"
-                [ngClass]="{
-                    'sxplr-d-none': (list.state$ | async) === 'noresult'
-                }">
-
-                <mat-expansion-panel-header>
-                    <mat-panel-title>
-                        <span class="sxplr-white-space-nowrap">
-                            {{ feature.name | featureNamePipe }}
-                        </span>
-                    </mat-panel-title>
-                </mat-expansion-panel-header>
-                
-                <spinner-cmp *ngIf="(list.state$ | async) === 'busy'"></spinner-cmp>
-                <sxplr-feature-list
-                    [template]="template"
-                    [parcellation]="parcellation"
-                    [region]="region"
-                    [bbox]="bbox"
-                    [queryParams]="queryParams | mergeObj : { type: (feature.name | featureNamePipe) }"
-                    [featureRoute]="feature.path"
-                    (onClickFeature)="onClickFeature($event)"
-                    #list="featureList"
-                    >
-                </sxplr-feature-list>
-
-            </mat-expansion-panel>
-        </mat-accordion>
-
-    </mat-expansion-panel>
-    
-
-    <!-- only show connectivity in human atlas for now -->
-    <ng-template [ngIf]="(selectedAtlas$ | async)?.species === 'Homo sapiens'">
-        <ng-template [ngIf]="cateogryCollections$ | async | keyvalue | filterCategory : ['connectivity']" let-connectivity>
-            <ng-template ngFor [ngForOf]="connectivity" let-conn>
-                <mat-expansion-panel sxplr-sapiviews-features-connectivity-check
-                    #connectivityAccordion
-                    *ngIf="conn">
-                    <mat-expansion-panel-header>
-                        <mat-panel-title>
-                            {{ conn.key }}
-                        </mat-panel-title>
-                    </mat-expansion-panel-header>
-    
-                    <sxplr-features-connectivity-browser class="pe-all flex-shrink-1" 
-                        [region]="region"
-                        [sxplr-features-connectivity-browser-atlas]="selectedAtlas$ | async"
-                        [sxplr-features-connectivity-browser-template]="template"
-                        [sxplr-features-connectivity-browser-parcellation]="parcellation"
-                        [accordionExpanded]="connectivityAccordion.expanded"
-                        [types]="conn.value">
-                    </sxplr-features-connectivity-browser>
-    
-                </mat-expansion-panel>
-            </ng-template>
-        </ng-template>
-    </ng-template>
-
-    
-    <mat-expansion-panel *ngFor="let keyvalue of (cateogryCollections$ | async | keyvalue | filterCategory : ['dataset', 'other'])"
-        sxplrCategoryAcc
-        #categoryAcc="categoryAcc"
-        [ngClass]="{
-            'sxplr-d-none': !(categoryAcc.isBusy$ | async) && (categoryAcc.total$ | async) === 0
-        }">
-
-        <mat-expansion-panel-header>
-
-            <mat-panel-title>
-                {{ keyvalue.key }}
-            </mat-panel-title>
-            
-            <mat-panel-description>
-                <spinner-cmp *ngIf="categoryAcc.isBusy$ | async"></spinner-cmp>
-                <ng-template [ngIf]="categoryAcc.total$ | async" let-total>
-                    <span>
-                        {{ total }}
-                    </span>
-                </ng-template>
-            </mat-panel-description>
-        </mat-expansion-panel-header>
-
-        <mat-accordion>
-            <mat-expansion-panel class="mat-elevation-z4"
-                *ngFor="let feature of keyvalue.value"
-                [ngClass]="{
-                    'sxplr-d-none': (list.state$ | async) === 'noresult'
-                }">
-
-                <mat-expansion-panel-header>
-                    <mat-panel-title>
-                        <span class="sxplr-white-space-nowrap">
-                            {{ feature.name | featureNamePipe }}
-                        </span>
-                    </mat-panel-title>
-                </mat-expansion-panel-header>
-                
-                <spinner-cmp *ngIf="(list.state$ | async) === 'busy'"></spinner-cmp>
-                <sxplr-feature-list
-                    [template]="template"
-                    [parcellation]="parcellation"
-                    [region]="region"
-                    [bbox]="bbox"
-                    [queryParams]="queryParams | mergeObj : { type: (feature.name | featureNamePipe) }"
-                    [featureRoute]="feature.path"
-                    (onClickFeature)="onClickFeature($event)"
-                    #list="featureList"
-                    >
-                </sxplr-feature-list>
-
-            </mat-expansion-panel>
-        </mat-accordion>
-
-    </mat-expansion-panel>
-    
-
-</mat-accordion>
diff --git a/src/features/entry/entry.nestedExpPanel.component.scss b/src/features/entry/entry.nestedExpPanel.component.scss
deleted file mode 100644
index 68a9f6d331b11a5b9723e3996f827c8240c1bae5..0000000000000000000000000000000000000000
--- a/src/features/entry/entry.nestedExpPanel.component.scss
+++ /dev/null
@@ -1,10 +0,0 @@
-mat-list-item
-{
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-
-sxplr-feature-list
-{
-    height: 10rem;
-}
diff --git a/src/features/list/list.component.html b/src/features/list/list.component.html
deleted file mode 100644
index f8316d76c83818f0330e12db6a5bbfdeff443ad4..0000000000000000000000000000000000000000
--- a/src/features/list/list.component.html
+++ /dev/null
@@ -1,33 +0,0 @@
-<ng-template [ngIf]="datasource$ | async" let-datasource>
-
-    <cdk-virtual-scroll-viewport itemSize="36"
-        (scrolledIndexChange)="onScroll(datasource, $event)"
-        class="virtual-scroll-viewport">
-        
-        <ng-template cdkVirtualFor
-            [cdkVirtualForOf]="datasource"
-            let-feature
-            let-last="last"
-            let-index="index">
-
-            <button
-                mat-button
-                class="virtual-scroll-item sxplr-w-100"
-                [matTooltip]="feature.name"
-                matTooltipPosition="right"
-                (click)="onClickItem(feature)">
-                {{ feature.name }}
-            </button>
-
-            <div *ngIf="last && datasource.isPulling$ | async">
-                <ng-template [ngTemplateOutlet]="loadingSpinnerTmpl">
-                </ng-template>
-            </div>
-
-        </ng-template>
-    </cdk-virtual-scroll-viewport>
-</ng-template>
-
-<ng-template #loadingSpinnerTmpl>
-    <spinner-cmp class="sxplr-pl-2 sxplr-d-block"></spinner-cmp>
-</ng-template>
diff --git a/src/features/list/list.component.scss b/src/features/list/list.component.scss
deleted file mode 100644
index ad15ab8305026cc915bde506021c9b8c558aa2d3..0000000000000000000000000000000000000000
--- a/src/features/list/list.component.scss
+++ /dev/null
@@ -1,27 +0,0 @@
-:host
-{
-    display: block;
-    width: 100%;
-    height:100%;
-}
-
-.feature-name
-{
-    white-space: nowrap;
-}
-
-.feature-name:hover
-{
-    cursor: default;
-}
-
-.virtual-scroll-viewport
-{
-    height: 100%;
-}
-
-.virtual-scroll-item
-{
-    height:36px;
-    display: block;
-}
diff --git a/src/features/list/list.component.spec.ts b/src/features/list/list.component.spec.ts
deleted file mode 100644
index dce8f1b840eb8c341090f0620fdbe8c50c3f0965..0000000000000000000000000000000000000000
--- a/src/features/list/list.component.spec.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { ComponentFixture, TestBed } from '@angular/core/testing';
-import { SAPIModule } from 'src/atlasComponents/sapi';
-
-import { ListComponent } from './list.component';
-
-describe('ListComponent', () => {
-  let component: ListComponent;
-  let fixture: ComponentFixture<ListComponent>;
-
-  beforeEach(async () => {
-    await TestBed.configureTestingModule({
-      imports: [
-        SAPIModule
-      ],
-      declarations: [ ListComponent ],
-    })
-    .compileComponents();
-
-    fixture = TestBed.createComponent(ListComponent);
-    component = fixture.componentInstance;
-    fixture.detectChanges();
-  });
-
-  it('should create', () => {
-    expect(component).toBeTruthy();
-  });
-});
diff --git a/src/features/list/list.component.ts b/src/features/list/list.component.ts
deleted file mode 100644
index f19f852f5cd96cd6a85165deecfcaa2e8a207c83..0000000000000000000000000000000000000000
--- a/src/features/list/list.component.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { Component, EventEmitter, Output } from '@angular/core';
-import { SAPI } from 'src/atlasComponents/sapi';
-import { Feature } from 'src/atlasComponents/sapi/sxplrTypes';
-import { ListDirective } from './list.directive';
-import { IsAlreadyPulling, PulledDataSource } from 'src/util/pullable';
-
-@Component({
-  selector: 'sxplr-feature-list',
-  templateUrl: './list.component.html',
-  styleUrls: ['./list.component.scss'],
-  exportAs: "featureList"
-})
-export class ListComponent extends ListDirective {
-
-  @Output()
-  onClickFeature = new EventEmitter<Feature>()
-
-  constructor(sapi: SAPI) {
-    super(sapi)
-  }
-
-  onClickItem(feature: Feature){
-    this.onClickFeature.emit(feature)
-  }
-
-  async onScroll(datasource: PulledDataSource<unknown>, scrollIndex: number){
-    if ((datasource.currentValue.length - scrollIndex) < 30) {
-      try {
-        await datasource.pull()
-      } catch (e) {
-        if (e instanceof IsAlreadyPulling) {
-          return
-        }
-        throw e
-      }
-    }
-  }
-}
diff --git a/src/features/module.ts b/src/features/module.ts
index 1e472a8dfb127c909fbeabb81d76b8674749a8fc..5cda100c6b993c838076043348603b31365ba3e0 100644
--- a/src/features/module.ts
+++ b/src/features/module.ts
@@ -9,7 +9,6 @@ import { SpinnerModule } from "src/components/spinner";
 import { UtilModule } from "src/util";
 import { EntryComponent } from './entry/entry.component'
 import { FeatureNamePipe } from "./featureName.pipe";
-import { ListComponent } from './list/list.component';
 import { CategoryAccDirective } from './category-acc.directive';
 import { SapiViewsFeatureConnectivityModule } from "./connectivity";
 import { ScrollingModule } from "@angular/cdk/scrolling";
@@ -50,7 +49,6 @@ import { GroupFeaturesToName } from "./grpFeatToName.pipe";
   ],
   declarations: [
     EntryComponent,
-    ListComponent,
     FeatureViewComponent,
     FilterCategoriesPipe,
     ListDirective,
diff --git a/src/util/fn.ts b/src/util/fn.ts
index 9a4ec27a97da209ee274edd562c01672ee11d1df..fd9316ab4d139cbf07d9bc17f4c8283ade502695 100644
--- a/src/util/fn.ts
+++ b/src/util/fn.ts
@@ -66,6 +66,49 @@ type TCacheFunctionArg = {
   serialization?: (...arg: any[]) => string
 }
 
+/**
+ * member function decorator
+ * can only be used to decorate arguementless async function
+ * 
+ */
+
+export const cachedPromise = <T>() => {
+  const key = Symbol('cachedpromise')
+  return (_target: any, _propertyKey: string, descriptor: TypedPropertyDescriptor<() => Promise<T>>) => {
+    const originalMethod = descriptor.value
+    descriptor.value = function() {
+      if (key in this) {
+        /**
+         * if cached promise exist, return cached promise
+         */
+        return this[key]
+      }
+      const cleanup = () => {
+        /**
+         * on cleanup, delete the stored instance
+         */
+        delete this[key]
+      }
+      const pr = new Promise<T>((rs, rj) => {
+        originalMethod.apply(this, [])
+          .then((val: T) => {
+            cleanup()
+            rs(val)
+          })
+          .catch((e: Error) => {
+            cleanup()
+            rj(e)
+          })
+      })
+      /**
+       * store the promise as a property of the instance
+       */
+      this[key] = pr
+      return pr
+    }
+  }
+}
+
 /**
  * Member function decorator
  * Multiple function calls with strictly equal arguments will return cached result
diff --git a/src/util/pullable.ts b/src/util/pullable.ts
index 2de10d4feea56907a3ab693ad2e6312b7208d76e..09ce6cec9c5961f1cff83513f93315c71fbe8b9b 100644
--- a/src/util/pullable.ts
+++ b/src/util/pullable.ts
@@ -1,6 +1,7 @@
 import { DataSource } from "@angular/cdk/collections"
-import { BehaviorSubject, Observable, ReplaySubject, Subscription, combineLatest, concat, of, pipe } from "rxjs"
+import { BehaviorSubject, Observable, ReplaySubject, Subscription, combineLatest, concat, of, timer } from "rxjs"
 import { finalize, map, scan, shareReplay, startWith, tap } from "rxjs/operators"
+import { cachedPromise } from "./fn"
 
 export interface IPuller<T> {
   next: (cb: (val: T) => void) => void
@@ -14,6 +15,7 @@ interface PaginatedArg<T> {
 }
 
 export class IsAlreadyPulling extends Error {}
+export class DsExhausted extends Error {}
 
 
 /**
@@ -25,15 +27,20 @@ export class PulledDataSource<T> extends DataSource<T> {
 
   protected annotations: Record<string, string>
 
-  protected onPulled() {
-    return pipe(
-    )
-  }
-
   #pull: () => Promise<T[]>
   
   completed = false
   private _data = new ReplaySubject<T[]>()
+  data$ = this._data.pipe(
+    startWith([] as T[]),
+    scan((acc, curr) => [...acc, ...curr]),
+    tap((v: T[]) => {
+      this.currentValue = v
+    }),
+    shareReplay(1)
+  )
+
+
   currentValue: T[] = []
   finalValue: T[] = []
 
@@ -50,10 +57,10 @@ export class PulledDataSource<T> extends DataSource<T> {
     return this._isPulling
   }
 
-
+  @cachedPromise()
   async pull(): Promise<T[]> {
     if (this.completed) {
-      return []
+      throw new DsExhausted()
     }
     if (this.isPulling) {
       throw new IsAlreadyPulling(`PulledDataSource is already pulling`)
@@ -67,7 +74,6 @@ export class PulledDataSource<T> extends DataSource<T> {
     this.isPulling = false
     if (newResults.length === 0) {
       this.complete()
-      return []
     }
     this._data.next(newResults)
     return newResults
@@ -85,13 +91,7 @@ export class PulledDataSource<T> extends DataSource<T> {
   }
 
   connect(): Observable<readonly T[]> {
-    return this._data.pipe(
-      startWith([] as T[]),
-      scan((acc, curr) => [...acc, ...curr]),
-      tap((v: T[]) => {
-        this.currentValue = v
-      }),
-    )
+    return this.data$
   }
   complete() {
     this.completed = true
@@ -106,6 +106,11 @@ export class PulledDataSource<T> extends DataSource<T> {
 
 export class ParentDatasource<T> extends PulledDataSource<T> {
 
+  private _data$ = new BehaviorSubject<T[]>([])
+  data$ = this._data$.pipe(
+    shareReplay(1),
+  )
+
   #subscriptions: Subscription[] = []
   _children: PulledDataSource<T>[] = []
   constructor(arg: PaginatedArg<T>){
@@ -114,13 +119,14 @@ export class ParentDatasource<T> extends PulledDataSource<T> {
     this._children = children
   }
 
+  @cachedPromise()
   async pull() {
     for (const ds of this._children) {
       if (!ds.completed) {
         return await ds.pull()
       }
     }
-    return []
+    throw new DsExhausted()
   }
 
   connect(): Observable<readonly T[]> {
@@ -131,25 +137,38 @@ export class ParentDatasource<T> extends PulledDataSource<T> {
     this.#subscriptions.push(
       combineLatest(this._children.map(c => c.isPulling$)).subscribe(flags => {
         this.isPulling = flags.some(flag => flag)
-      })
-    )
-
-    return concat(
-      ...this._children.map(ds => ds.connect())
-    ).pipe(
-      map(arr => {
-        const alreadyEmitted = this._children.filter(c => c.completed)
-        const prevValues = alreadyEmitted.flatMap(v => v.finalValue)
-        return [...prevValues, ...arr]
       }),
-      
-      tap((v: T[]) => {
+      concat(
+        ...this._children.map(ds => ds.connect()),
+        /**
+         * final emitted value
+         * in some circumstances, all children would have been completed. 
+         * the first synchronous empty array flushes the current value
+         * the second timed empty array completes the observable
+         * 
+         * Observable must not be completed synchronously, as this leads to the final value not emitted. 
+         */
+        of([] as T[]).pipe(
+          tap(() => this.finalValue = this.currentValue)
+        ),
+        timer(160).pipe(
+          map(() => [] as T[])
+        )
+      ).pipe(
+        map(arr => {
+          const alreadyCompleted = this._children.filter(c => c.completed)
+          const prevValues = alreadyCompleted.flatMap(v => v.finalValue)
+          return [...prevValues, ...arr]
+        }),
+        finalize(() => {
+          this._data$.complete()
+        })
+      ).subscribe(v => {
         this.currentValue = v
-      }),
-      finalize(() => {
-        this.finalValue = this.currentValue || []
+        this._data$.next(v)
       })
     )
+    return this.data$
   }
 
   disconnect(): void {
diff --git a/src/viewerModule/module.ts b/src/viewerModule/module.ts
index 22a5950b8af7a1132d9ce4ddaf835b39bd1eceb7..da2c56f121e5448e0dea34081de8520d150cb40b 100644
--- a/src/viewerModule/module.ts
+++ b/src/viewerModule/module.ts
@@ -33,6 +33,7 @@ import { ATPSelectorModule } from "src/atlasComponents/sapiViews/core/rich/ATPSe
 import { FeatureModule } from "src/features";
 import { NgLayerCtlModule } from "./nehuba/ngLayerCtlModule/module";
 import { SmartChipModule } from "src/components/smartChip";
+import { ReactiveFormsModule } from "@angular/forms";
 
 @NgModule({
   imports: [
@@ -58,6 +59,7 @@ import { SmartChipModule } from "src/components/smartChip";
     FeatureModule,
     NgLayerCtlModule,
     SmartChipModule,
+    ReactiveFormsModule,
     ...(environment.ENABLE_LEAP_MOTION ? [LeapModule] : [])
   ],
   declarations: [
diff --git a/src/viewerModule/viewerCmp/viewerCmp.component.ts b/src/viewerModule/viewerCmp/viewerCmp.component.ts
index b19b8224e9b8ea1117baacc101101e233564b184..fc1a4b87b41f40d0d3d594d8559b32b64edadeff 100644
--- a/src/viewerModule/viewerCmp/viewerCmp.component.ts
+++ b/src/viewerModule/viewerCmp/viewerCmp.component.ts
@@ -1,7 +1,7 @@
 import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, TemplateRef, ViewChild, ViewContainerRef } from "@angular/core";
 import { select, Store } from "@ngrx/store";
-import { combineLatest, Observable, Subscription } from "rxjs";
-import { debounceTime, map, shareReplay, startWith } from "rxjs/operators";
+import { BehaviorSubject, combineLatest, Observable, Subscription } from "rxjs";
+import { debounceTime, map, shareReplay, startWith, switchMap } from "rxjs/operators";
 import { CONST, ARIA_LABELS, QUICKTOUR_DESC } from 'common/constants'
 import { animate, state, style, transition, trigger } from "@angular/animations";
 import { IQuickTourData } from "src/ui/quickTour";
@@ -12,6 +12,8 @@ import { SAPI } from "src/atlasComponents/sapi";
 import { Feature, SxplrAtlas, SxplrRegion } from "src/atlasComponents/sapi/sxplrTypes"
 import { atlasAppearance, atlasSelection, userInteraction } from "src/state";
 import { SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes";
+import { FormControl } from "@angular/forms";
+import { EntryComponent } from "src/features/entry/entry.component";
 
 @Component({
   selector: 'iav-cmp-viewer-container',
@@ -60,6 +62,9 @@ import { SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes";
 
 export class ViewerCmp implements OnDestroy {
 
+  @ViewChild('voiFeatureEntryCmp', { read: EntryComponent })
+  voiCmp: EntryComponent
+
   public CONST = CONST
   public ARIA_LABELS = ARIA_LABELS
   
@@ -250,6 +255,24 @@ export class ViewerCmp implements OnDestroy {
     this.onDestroyCb.push(
       () => this.ctxMenuSvc.deregister(cb)
     )
+
+    
+    this.subscriptions.push(
+      this.showVOIWireframeSlideToggle.valueChanges.pipe(
+        switchMap(showWireFrame => this.voiCmp.totals$.pipe(
+          map(totals => ({
+            totals,
+            showWireFrame
+          }))
+        ))
+      ).subscribe(async ({ showWireFrame }) => {
+        if (showWireFrame) {
+          this._loadingVoiWireFrame$.next(true)
+          await this.voiCmp.pullAll()
+        }
+        this._loadingVoiWireFrame$.next(false)
+      })
+    )
   }
 
   ngOnDestroy(): void {
@@ -340,4 +363,8 @@ export class ViewerCmp implements OnDestroy {
       })
     )
   }
+
+  showVOIWireframeSlideToggle = new FormControl<boolean>(false)
+  private _loadingVoiWireFrame$ = new BehaviorSubject<boolean>(false)
+  loadingVoiWireFrame$ = this._loadingVoiWireFrame$.asObservable()
 }
diff --git a/src/viewerModule/viewerCmp/viewerCmp.template.html b/src/viewerModule/viewerCmp/viewerCmp.template.html
index 19914ede12cbe2b99d2be1d8b227000aefca2dd7..ac8aad7a437c94dce8fd2325f58086c6cc73daff 100644
--- a/src/viewerModule/viewerCmp/viewerCmp.template.html
+++ b/src/viewerModule/viewerCmp/viewerCmp.template.html
@@ -975,6 +975,13 @@
         </ng-template>
       </mat-card-subtitle>
     </mat-card-header>
+
+    <mat-slide-toggle [formControl]="showVOIWireframeSlideToggle">
+      <span>
+        Show VOI Wireframe
+      </span>
+      <spinner-cmp class="sxplr-d-inline-block" *ngIf="loadingVoiWireFrame$ | async"></spinner-cmp>
+    </mat-slide-toggle>
   </mat-card>
 
   <sxplr-feature-entry
@@ -1014,12 +1021,12 @@
   this has been temporarily disabled, since datasource is paginated 
   and how bounding boxes are drawn needs to be reconsidered -->
   
-  <!-- <div
-    *ngIf="voiSwitch.switchState$ | async"
+  <div
+    *ngIf="showVOIWireframeSlideToggle.valueChanges | async"
     voiBbox
     [features]="voiFeatureEntryCmp.features$ | async">
 
-  </div> -->
+  </div>
 </ng-template>
 
 <div