8

I am using Angular 11 and I am accessing an observable in the template of my component with async pipe.

First load of the route, everything works perfectly fine. No error. When I navigate away from the page and come back, I get following error:

Component Template: <RM-map *ngIf="(layers$ | async) as layers" [layers]="layers.layerConfig" showLayersPanel="true" id="RIS-map">

Component

import { Component, OnDestroy, OnInit } from '@angular/core';
import { Observable, Subject } from 'rxjs';

import { FullMapViewService } from '../services/full-map-view.service';
import { RISLayerConfigResponse } from '@RM/interfaces';
import { takeUntil } from 'rxjs/operators';

@Component({
  selector: 'RM-full-map-view',
  templateUrl: './full-map-view.component.html',
  styleUrls: ['./full-map-view.component.scss']
})
export class FullMapViewComponent implements OnInit, OnDestroy {
  layers$: Observable<RISLayerConfigResponse>;
  destroyed$: Subject<boolean> = new Subject();
  constructor(private fullMapViewService: FullMapViewService) {}

  ngOnInit(): void {
    this.fullMapViewService.setParamsRequiredForRIS();
    this.fullMapViewService.initializeRISLayerCreationService();
    this.layers$ = this.fullMapViewService
      .getLayersForAllProjects()
      .pipe(takeUntil(this.destroyed$));
  }

  ngOnDestroy() {
    this.destroyed$.next(true);
  }
}

full-map-view.service.ts

import { DEPLOYMENT_PATH, SET_FROM_SERVER } from '@XYZ/RIS';
import {
  DataSet,
  DatasetsAndLayerConfig,
  RISLayerConfigResponse,
  RISLayerSettingsWithKind,
  Layer,
  LayerConfig,
  UpdateViewVCS
} from '@XYZ/interfaces';
import { Injectable, OnDestroy } from '@angular/core';
import { Observable, Subscription } from 'rxjs';

import { API_PATHS } from '../../api-paths';
import { BaseAPIService } from '@XYZ/core';
import { ClauseGenerationUtility } from '../../utils/clause-generation/clause-generation.util';
import { RISLayerCreationService } from '@ABC-innersource/RIS-canvas';
import { LAYER_COLOR_PALLET } from '../../services/services.constant';
import { map } from 'rxjs/operators';

@Injectable()
export class FullMapViewService implements OnDestroy {
  layersMappingConfiguration: {};
  layers: LayerConfig;
  private clauseGenerator = new ClauseGenerationUtility();
  private addUpdateVCSForKindSubscriptions: Subscription[];
  private initializeRISLayerCreationServiceSubscription: Subscription;
  private deploymentUrl: string;
  private appKey: string;
  private ABCDataPartitionId: string;
  private sToken: string;

  constructor(
    private baseAPIService: BaseAPIService,
    private layerCreationService: RISLayerCreationService
  ) {}

  // eslint-disable-next-line max-lines-per-function
  getLayersForAllProjects(): Observable<RISLayerConfigResponse> {
    return this.baseAPIService
      .get(API_PATHS.LAYERS.GET_LAYERS + '/projects/all')
      .pipe(
        map((res: DatasetsAndLayerConfig) => {
          return res;
        }),
        // eslint-disable-next-line max-lines-per-function
        map((datasetsAndLayerConfig: DatasetsAndLayerConfig) => {
          const datasets = [...datasetsAndLayerConfig.datasets];
          const notConfiguredKinds = [
            ...datasetsAndLayerConfig.layerConfig.notConfiguredKinds
          ];
          const notConfiguredKindsLayers = this.getNonConfiguredKindsLayers(
            notConfiguredKinds
          );
          const layers = this.combineLayersAndNotConfiguredKindsLayers(
            datasetsAndLayerConfig.layerConfig.layerConfig,
            notConfiguredKindsLayers
          );
          const kindsLayersHashmap = this.getKindsLayersHashmap(layers);
          const layersByDatasets = datasets
            .map((dataset: DataSet) => {
              return {
                ...this.updateLayersWithDatasetNameAndClauses(
                  kindsLayersHashmap,
                  dataset
                )
              };
            })
            .filter((layer) => {
              return Object.keys(layer).length !== 0;
            })
            .map((layer, index) => {
              return {
                ...this.assignColourToLayer(layer, index)
              };
            });
          return {
            layerConfig: layersByDatasets,
            notConfiguredKinds: []
          };
        })
      );
  }

  setParamsRequiredForRIS(): void {
    this.sToken = SET_FROM_SERVER;
    this.deploymentUrl = DEPLOYMENT_PATH;
    this.appKey = SET_FROM_SERVER;
    this.ABCDataPartitionId = SET_FROM_SERVER;
  }

  initializeRISLayerCreationService(): void {
    this.initializeRISLayerCreationServiceSubscription = this.layerCreationService
      .initialize(
        this.sToken,
        this.deploymentUrl,
        this.appKey,
        this.ABCDataPartitionId
      )
      .subscribe();
  }

  ngOnDestroy(): void {
    this.initializeRISLayerCreationServiceSubscription.unsubscribe();
    this.addUpdateVCSForKindSubscriptions.forEach(
      (subscription: Subscription) => {
        subscription.unsubscribe();
      }
    );
  }

  private updateLayersWithDatasetNameAndClauses(
    kindsLayersHashmap: Map<string, RISLayerSettingsWithKind>,
    dataset: DataSet
  ): RISLayerSettingsWithKind {
    const currentDataset = { ...dataset };
    const datasetKind = this.generateKindFromDataset(currentDataset);
    const layer = kindsLayersHashmap.get(datasetKind);
    const queryRef = this.getFormattedQuery(
      currentDataset.dataSetDefinition.queryDefinition.queryRef
    );
    const clause = this.clauseGenerator.generateClause(queryRef);

    if (!layer) {
      return undefined;
    }

    layer.name = currentDataset.name;
    layer.tableInfo.where = clause;
    return JSON.parse(JSON.stringify(layer));
  }

  private generateKindFromDataset(dataset: DataSet): string {
    const currentDataset = { ...dataset };
    const datasetQueryDefinition =
      currentDataset.dataSetDefinition.queryDefinition;
    return `${datasetQueryDefinition.authority}:${datasetQueryDefinition.source}:${datasetQueryDefinition.entity}:${datasetQueryDefinition.version}`;
  }

  private getKindsLayersHashmap(
    layers: RISLayerSettingsWithKind[]
  ): Map<string, RISLayerSettingsWithKind> {
    const kindsLayersHashmap = new Map();
    const allLayers = [...layers];
    allLayers.forEach((layer: RISLayerSettingsWithKind) => {
      kindsLayersHashmap.set(layer.kind, layer);
    });
    return kindsLayersHashmap;
  }

  private getNonConfiguredKindsLayers(
    kinds: string[]
  ): RISLayerSettingsWithKind[] {
    const notConfiguredKindsLayers: RISLayerSettingsWithKind[] = [];
    kinds.forEach((kind) => {
      const layer: RISLayerSettingsWithKind[] = this.layerCreationService.getLayerInfoByKindName(
        kind
      ) as RISLayerSettingsWithKind[];
      if (layer.length > 0) {
        layer[0].kind = kind;
        notConfiguredKindsLayers.push(layer[0]);
        this.addUpdateRISLayerInVCS({ kind: kind, configuration: layer[0] });
      }
    });
    return notConfiguredKindsLayers;
  }

  private addUpdateRISLayerInVCS(layer: Layer): void {
    const currentLayer = { ...layer };
    const updateViewPayload: UpdateViewVCS = {
      control: 'RIS',
      definition: [{ ...currentLayer.configuration }]
    };
    this.addUpdateVCSForKind(currentLayer.kind, updateViewPayload);
  }

  private addUpdateVCSForKind(kind: string, payload: UpdateViewVCS): void {
    const subscription = this.baseAPIService
      .post(
        `${API_PATHS.CONFIG.VIEW.UPDATE_RIS_VIEW_CONFIG}`.replace(
          '${kind}',
          kind
        ),
        payload
      )
      .subscribe();
    this.addUpdateVCSForKindSubscriptions.push(subscription);
  }

  private combineLayersAndNotConfiguredKindsLayers(
    layers: RISLayerSettingsWithKind[],
    notConfiguredKindsLayers: RISLayerSettingsWithKind[]
  ): RISLayerSettingsWithKind[] {
    const allLayers = [...layers];
    const allNotConfiguredKindsLayers = [...notConfiguredKindsLayers];
    return [...allLayers, ...allNotConfiguredKindsLayers];
  }

  private getFormattedQuery(query: string): string {
    let formattedQuery = '';
    if (
      this.clauseGenerator.hasAndOperator(query) ||
      this.clauseGenerator.hasOrOperator(query)
    ) {
      formattedQuery = this.clauseGenerator.isWrappedWithRoundBrackets(query)
        ? query
        : `(${query})`;
      return formattedQuery;
    }
    return formattedQuery;
  }

  private assignColourToLayer(
    layer: RISLayerSettingsWithKind,
    index: number
  ): RISLayerSettingsWithKind {
    const colors = LAYER_COLOR_PALLET;
    const currentLayer = JSON.parse(JSON.stringify(layer));
    currentLayer.style.rules[0].style.fillColor = colors[index];
    currentLayer.style.rules[0].style.borderColor = '#000';
    return currentLayer;
  }
}

For e.g. route B is my component containing the observable A ---> B the observable loads perfectly fine. B ----> A and again A ----> B the observable throws below error.

ObjectUnsubscribedErrorImpl {message: "object unsubscribed", name: "ObjectUnsubscribedError"}
message: "object unsubscribed"
name: "ObjectUnsubscribedError"

Full stack trace snapshot is as shown below:

    core.js:6162 ERROR ObjectUnsubscribedErrorImpl {message: "object unsubscribed", name: "ObjectUnsubscribedError"}message: "object unsubscribed"name: "ObjectUnsubscribedError"__proto__: Error

defaultErrorLogger  @   core.js:6162
handleError @   core.js:6210
(anonymous) @   core.js:29503
invoke  @   zone-evergreen.js:364
run @   zone-evergreen.js:123
runOutsideAngular   @   core.js:28439
tick    @   core.js:29503
(anonymous) @   core.js:29372
invoke  @   zone-evergreen.js:364
onInvoke    @   core.js:28510
invoke  @   zone-evergreen.js:363
run @   zone-evergreen.js:123
run @   core.js:28394
next    @   core.js:29371
schedulerFn @   core.js:25848
__tryOrUnsub    @   Subscriber.js:183
next    @   Subscriber.js:122
_next   @   Subscriber.js:72
next    @   Subscriber.js:49
next    @   Subject.js:39
emit    @   core.js:25838
checkStable @   core.js:28447
onLeave @   core.js:28560
onInvokeTask    @   core.js:28504
invokeTask  @   zone-evergreen.js:398
runTask @   zone-evergreen.js:167
invokeTask  @   zone-evergreen.js:480
invokeTask  @   zone-evergreen.js:1621
globalZoneAwareCallback @   zone-evergreen.js:1658
load (async)        
customScheduleGlobal    @   zone-evergreen.js:1773
scheduleTask    @   zone-evergreen.js:385
onScheduleTask  @   zone-evergreen.js:272
scheduleTask    @   zone-evergreen.js:378
scheduleTask    @   zone-evergreen.js:210
scheduleEventTask   @   zone-evergreen.js:236
(anonymous) @   zone-evergreen.js:1928
(anonymous) @   http.js:1805
_trySubscribe   @   Observable.js:42
subscribe   @   Observable.js:28
call    @   catchError.js:14
subscribe   @   Observable.js:23
call    @   catchError.js:14
subscribe   @   Observable.js:23
innerSubscribe  @   innerSubscribe.js:67
_innerSub   @   mergeMap.js:57
_tryNext    @   mergeMap.js:51
_next   @   mergeMap.js:34
next    @   Subscriber.js:49
(anonymous) @   subscribeToArray.js:3
_trySubscribe   @   Observable.js:42
subscribe   @   Observable.js:28
call    @   mergeMap.js:19
subscribe   @   Observable.js:23
call    @   filter.js:13
subscribe   @   Observable.js:23
call    @   map.js:16
subscribe   @   Observable.js:23
call    @   map.js:16
subscribe   @   Observable.js:23
call    @   map.js:16
subscribe   @   Observable.js:23
createSubscription  @   common.js:4224
_subscribe  @   common.js:4305
transform   @   common.js:4292
ɵɵpipeBind1 @   core.js:25718
FullMapViewComponent_Template   @   full-map-view.component.html:2
executeTemplate @   core.js:9549
refreshView @   core.js:9418
refreshComponent    @   core.js:10584
refreshChildComponents  @   core.js:9215
refreshView @   core.js:9468
refreshEmbeddedViews    @   core.js:10538
refreshView @   core.js:9442
refreshEmbeddedViews    @   core.js:10538
refreshView @   core.js:9442
refreshComponent    @   core.js:10584
refreshChildComponents  @   core.js:9215
refreshView @   core.js:9468
renderComponentOrTemplate   @   core.js:9532
tickRootContext @   core.js:10758
detectChangesInRootView @   core.js:10783
detectChanges   @   core.js:22751
tick    @   core.js:29491
(anonymous) @   core.js:29372
invoke  @   zone-evergreen.js:364
onInvoke    @   core.js:28510
invoke  @   zone-evergreen.js:363
run @   zone-evergreen.js:123
run @   core.js:28394
next    @   core.js:29371
schedulerFn @   core.js:25848
__tryOrUnsub    @   Subscriber.js:183
next    @   Subscriber.js:122
_next   @   Subscriber.js:72
next    @   Subscriber.js:49
next    @   Subject.js:39
emit    @   core.js:25838
checkStable @   core.js:28447
onHasTask   @   core.js:28527
hasTask @   zone-evergreen.js:419
_updateTaskCount    @   zone-evergreen.js:440
_updateTaskCount    @   zone-evergreen.js:263
runTask @   zone-evergreen.js:184
drainMicroTaskQueue @   zone-evergreen.js:569
invokeTask  @   zone-evergreen.js:484
invokeTask  @   zone-evergreen.js:1621
globalZoneAwareCallback @   zone-evergreen.js:1647

If you see, FullMapViewComponent_Template @ full-map-view.component.html:2 mentions an issue with the observable on the template.

I am unsure how to handle this. This template is on Route B.

Ankit Tanna
  • 1,779
  • 8
  • 32
  • 59

6 Answers6

17

I searched for the places where the ObjectUnsubscribedError is thrown in the rxjs github project and tried to get insights when this error is actually thrown. I could find the best insight at the this test. I try to summarize how I understand the behavior:

When you unsubscribe directly from a subject it will be closed. Whenever you then try to resubscribe to it it will throw the ObjectUnsubscribedError.

That being known means you most likely hold your Subject (in your service) although your component is thrown away and unsubscribes. When you re-route to the component it tries to subscribe again and then the error gets thrown. I have a minimal reproducible example that I took from the linked test above:

const { Subject } = rxjs;

const subject = new Subject();

subject.subscribe()

subject.next("foo")
subject.unsubscribe()

try {
  subject.subscribe()
} catch (e) {
  console.error("the error: ", e)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.3/rxjs.umd.min.js"></script>

Now that we know how the error might be thrown a possible solution could be to only unsubscribe on a subscription in your component and next the value in a new component specific subject.

Pseudo sample code:

// Your subscription on wich you can unsubscribe
private copySubjectSubscription;
// This subject is used in your component with the async pipe
public readonly copySubject = new Subject();

constructor(serviceWithSubject) {
  this.copySubjectSubscription =
    serviceWithSubject.subject.subscribe(e => copySubject.next())
}

ngOnDestroy() {
  this.copySubjectSubscription.unsubscribe();
}

As I am not into your code and also have limited time you might find a more elegant solution to copy the subject and its values without directly unsubscribing.

I personally worked a lot with rxjs and haven't had this issue ever. Maybe I could always avoid this behavior by a different design approach as I always try to create and destroy Observables in components.

Jonathan Stellwag
  • 3,843
  • 4
  • 25
  • 50
3

Async pipes automatically unsubscribe to the observable when the component is destroyed. So, you don't need to use takeUntil method. This will also remove the overhead of creating an extra rxjs subject.

import { Component, OnDestroy, OnInit } from '@angular/core';
import { Observable, Subject } from 'rxjs';

import { FullMapViewService } from '../services/full-map-view.service';
import { RISLayerConfigResponse } from '@RM/interfaces';
import { takeUntil } from 'rxjs/operators';

@Component({
  selector: 'RM-full-map-view',
  templateUrl: './full-map-view.component.html',
  styleUrls: ['./full-map-view.component.scss']
})
export class FullMapViewComponent implements OnInit, OnDestroy {
  layers$: Observable<RISLayerConfigResponse>;
  constructor(private fullMapViewService: FullMapViewService) {}

  ngOnInit(): void {
    this.fullMapViewService.setParamsRequiredForRIS();
    this.fullMapViewService.initializeRISLayerCreationService();
    this.layers$ = this.fullMapViewService
      .getLayersForAllProjects();
  }

}

Also, if you want to use the takeUntil approach, then in the ngOnDestry function, after emitting the value, you will need to mark that subject as complete.

ngOnDestroy() {
    this.destroy$.next(true);
    this.destroy$.complete();
}
Drashti Dobariya
  • 2,455
  • 2
  • 10
  • 23
1

A clean approach for unsubscribe observables that i suggest you is something like this:

import { takeUntil } from 'rxjs/operators';
import { Subject } from 'rxjs';
...
private _unsubscribeSignal$: Subject<void> = new Subject();
...
ngOnDestroy() {
   this._unsubscribeSignal$.next();
   this._unsubscribeSignal$.unsubscribe();
}
...
// Somewhere
new Observable() // Here your observable
  .pipe(takeUntil(this._unsubscribeSignal$.asObservable()))
  .subscribe(
    (result) => {

    }, (err) => {

    }
  );

Note edit: the takeUntil is making the job.

1

//For some reason, you have to declare your subscription

import { Subscription } from 'rxjs';

private yourSubscription!: Subscription; 

ngOnInit() {
this.yourSubscription = this.someService.subscribe(() => {})

}

ngOnDestroy() {
  this.yourSubscription.unsubscribe();
} 

//It worked for me, I hope I helped!

Pavke987
  • 11
  • 1
0

Can you try below

export class FullMapViewComponent implements OnInit, OnDestroy {
  layers$: Observable<RISLayerConfigResponse>;
  destroyed$: Subject<boolean> = new Subject();

  subscriptionsObserver: Subcription;
  constructor(private fullMapViewService: FullMapViewService) {
    this.subscriptionsObserver = new Subscription();
}

  ngOnInit(): void {
    this.fullMapViewService.setParamsRequiredForRIS();
    this.fullMapViewService.initializeRISLayerCreationService();
    this.layers$ = this.fullMapViewService
      .getLayersForAllProjects();
    this.subcriptionsObserver.add(this.layers$);
  }

  ngOnDestroy() {
   // this.destroyed$.next(true);
    this.subscriptionsObserver.unsubscribe();


  }
}
mak15
  • 367
  • 2
  • 12
0

In my case, I was getting this error when I did async subscription in an <ng-template>. After I passed the async subscription through context, it worked fine. Hopefully, this helps someone.

Before

<ng-container *ngTemplateOutlet="modal"></ng-container>

<ng-template #modal let-data>
<!-- Had the error when using directly here -->
{{(obs | async)?.data}}
<ng-template>

After

<ng-container *ngTemplateOutlet="modal; context:{$implicit: (obs | async)?.data}"></ng-container>

<ng-template #modal let-data>
<!-- fine to use data here, no error -->
{{data}}
<ng-template>

obs is an observable from the component class

BrunoElo
  • 360
  • 6
  • 11