Pausing or buffering an RxJS 6 observable when the page is inactive

Currently, I am dealing with a stream of letters that need to be arranged in the correct order to form a word. However, an issue arises when the user switches tabs, minimizes the browser, or switches applications - the behavior mimics using setTimeout(), resulting in a jumbled order and lost items. Despite attempting to utilize functions like bufferWhen(), bufferToggle(), takeUntil(), publish(), and connect(), none of them have helped me achieve my objective. While considering using delayWhen, it seems deprecated and not practical as it immediately halts the stream. Could you suggest which functions I should use and how? Here's the snippet of my code:

export class MyComponent implements AfterViewInit {
  private visibilityChange$ = fromEvent(document, 'visibilitychange').pipe(startWith('visible'), shareReplay({ refCount: true, bufferSize: 1 }));
  private show$ = this.visibilityChange$.pipe(filter(() => document.visibilityState === 'visible'));
  private hide$ = this.visibilityChange$.pipe(filter(() => document.visibilityState === 'hidden'));

  public ngAfterViewInit() {
    const lettersStream$ = zip( // add delay for each letter
            from(['w', 'o', 'r', 'd']),
            interval(1000))
           // pause when hide$ fires, resume when show$
          .pipe(map(([letter, delayTime]) => letter))
          .subscribe(console.log);
  }
}

I have created a demonstration on stackblitz with the main goal being to observe (and halt writing when tab is inactive) how the phrase appears on the screen.

Answer №1

In a previous project called RxJS Snake Game, I implemented a similar pause/unpause feature which can be helpful for your case.

The core idea is to use an interval(1000) as the foundation, serving as the source of truth for all actions. The goal then becomes to make this interval pausable based on visibility changes: emitting events when visible and halting on hide. By simply ceasing to listen to the source interval during hide and resuming on show, the implementation can be simplified. Let's delve into the exact approach:

To experiment with the modified StackBlitz demo code, visit RxJS Pause Observable.

import { of, interval, fromEvent, timer, from, zip, never } from 'rxjs';
import { delayWhen, tap, withLatestFrom, concatMap, take, startWith, distinctUntilChanged, switchMap, shareReplay, filter, map, finalize } from 'rxjs/operators';

console.log('-------------------------------------- STARTING ----------------------------')

class MyComponent {
  private visibilityChange$ = fromEvent(document, 'visibilitychange')
    .pipe(
      map(x => document.visibilityState),
      startWith('visible'),
      shareReplay(1)
    );

  private isVisible$ = this.visibilityChange$.pipe(
    map(x => x === 'visible'),
    distinctUntilChanged(),
  );

  constructor() {
    const intervalTime = 1000;
    const source$ = from('word or two'.split(''));
    /** should remove these .pipe(
        concatMap(ch => interval(intervalTime).pipe(map(_ => ch), take(1)))
      );*/

    const pausableInterval$ = this.isVisible$.pipe(
      switchMap(visible => visible ? interval(intervalTime) : never()),
    )

    const lettersStream$ = zip(pausableInterval$, source$).pipe(
      map(([tick, letter]) => letter),
    ).subscribe(letter => {
      this.writeLetter(letter);
    });
  }

  private writeLetter(letter: string) {
    if (letter === ' ') letter = '\u00A0'; // fix for spaces
    document.body.innerText += letter;
  }
}

const component = new MyComponent();

This snippet above showcases code directly pulled from StackBlitz, replicated here for better understanding.

Let's break down some key components:

  1. Take note of visibilityChange$ and isVisible$. They are slightly altered to emit either 'visible' or 'hidden' depending on document.visibilityState. The latter outputs true when document.visibilityState is 'visible'.

  2. Observe the behavior of source$. It emits a letter and pauses for 1 second using concatMap and interval combined with take(1), repeating this process until no characters remain in the text.

  3. Focus on pausableInterval$. This observable, driven by this.isVisible$ that adjusts according to document.visibilityState, will output items every second or nothing at all thanks to never().

  4. Lastly, check out lettersStream$. Utilizing zip(), it pairs up pausableInterval$ and source$, retrieving one letter from the source alongside a tick from the pausable interval. If pausableInterval$ stops emitting due to a visibility change, zip will also wait, requiring both observables to emit concurrently to trigger a subscription event.

Answer №2

Feeling a bit unsure about how to tackle this scenario, but here is a possible solution:

Firstly, start by implementing the following:

private isVisible$ = this.visibilityChange$.pipe(
                       filter(() => document.visibilityState === 'visible'), 
                       distinctUntilChanged()); // just a precautionary step

Next, proceed with the following:

const lettersStream$ = this.isVisible$.pipe(
        switchMap((isVisible) => (isVisible)
          ? zip( // introducing a delay for each letter
              from(['w', 'o', 'r', 'd']),
              interval(1000))
            .pipe(map(([letter, delayTime]) => letter))
          : NEVER
        )
      ).subscribe(console.log);

Essentially, the switchMap function should be triggered every time the visibility changes - if visible, subscribe to the source, otherwise do nothing.

In this example context, the outcome might not be entirely accurate because the from() function will consistently emit the same sequence. However, in a real-world situation with a non-static source, it should perform as expected.

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

The parameter 'prev: todoType[] => todoType[]' cannot be assigned to the type 'todoType[]'.ts(2345)

An issue has arisen with this.props.update as mentioned in the title import { useState } from "react"; import axios from "axios"; import { todoType } from "../../types/todo"; type sendResponse = { task: string; }; type getRe ...

Encountered an issue while trying to install ngrx store with Angular 13 - unable to resolve the dependency

Encountering an error with the following command: ng add @ngrx/store@latest The error message reads as follows: npm ERR! code ERESOLVE npm ERR! ERESOLVE unable to resolve dependency tree npm ERR! npm ERR! While resolving: <a href="/cdn-cgi/l/email-prot ...

Error occurs when using JSON.stringify with a Typescript array map

When running this code snippet in Typescript: [].map(JSON.stringify); An error is being thrown: Argument of type '{ (value: any, replacer?: ((key: string, value: any) => any) | undefined, space?: string | number | undefined): string; (value: a ...

Unable to download essential dependencies using NPM

After cloning a repository for a MEAN stack application, the first step was to run npm install. The installation process resulted in: added 1580 packages from 1887 contributors and audited 15249 packages in 281.586s found 18 vulnerabilities (5 low, 12 mod ...

"Learn how to dynamically update a value based on the selected option in a drop-down menu in Angular

Hello everyone. I am working on an angular project that includes a product page. Some products on this page can have multiple types, like so: Product ID-1 Type ID-1 Price-$10 Product ID-1 Type ID-2 Price-$15 Product ID-1 Type ID-3 Price-$25 In this sce ...

Sending properties of an element to a function within Angular version 4 or 5

Trying to pass attribute values of elements to a function on button click, this is my approach: <div> <ul #list> <li class="radio" *ngFor="let option of options; let j = index" id={{i}}-{{j}} #item> <label><input t ...

Using Typescript generics to create parameter and argument flexibility for both classes and

I'm facing an issue where I need to effectively chain multiple function calls and ensure that TypeScript verifies the correctness of their linkage. export class A<T, K> { public foo(a: A<K, T>): A<K, T> { return a; } } cons ...

"Endless loop conundrum in ngrx router

Currently, I am facing an issue while trying to integrate ngrx router-store into my application. After referencing the example app on GitHub, it seems that adding a `routerReducer` property to my application state and including the `routerReducer` from ngr ...

Angular - Loading images on the fly

After scouring numerous resources, I couldn't find a resolution to my issue. For your information, I am utilizing ASP.net Core 2.0's default angular project In the process of developing an Angular application, I am faced with the challenge of ...

How do I implement branch code using TypeScript types in Svelte?

Looking for a solution similar to the one mentioned in Typescript: How to branch based on type, but tailored for Svelte. Despite implementing run-time type guards as suggested, Svelte continues to throw errors. I am dealing with an array called collectabl ...

The method to create a global generic class in TypeScript

Is there a way to globally expose the Hash class? Access Playground here export {} class Hash<K, V> { } declare global { // How can we achieve this? } window.Hash = Hash // Making it globally accessible ...

Visualize complex JSON data within an Angular Material Table

The JSON file is structured as follows: { "products": { "items":[ { "productId": "1", "dept": "home", "itemtype": "small" }, { "productId": "2", "dept": "kitchen", "itemtype": "medium" ...

Exploring the functionality of the scan operator within switchMap/mergeMap in RxJS

We're utilizing the scan operator to handle our 'load more' button within our table. This operator allows us to accumulate new results with the previous ones, but we've come across some unexpected behavior. Let's break it down by l ...

Encountering issues when passing a string as query parameters

How can I successfully pass a string value along with navigation from one component to another using query parameters? Component1: stringData = "Hello"; this.router.navigate(['component2'], { queryParams: stringData }); Component2: ...

How can I make angular material data table cells expand to the full width of content that is set to nowrap?

This example demonstrates how the mat-cells are styled with a specific width: .mat-cell { white-space: nowrap; min-width: 150rem; } If the width is not specified, the table will cut off the text due to the white-space property being set to nowrap. Is ...

Creating HTML code using an array of objects

My goal is to create HTML tags based on the object response shown below: "view": [{ "type": 'text', "depth": 0, "text": "This is a sample text" }] The objective here is to iterate through each type and add the corresponding HTML tags. &l ...

Receiving real-time updates every second from the API

I am currently working on an Angular project where I need to continuously make API calls to retrieve information from a service. Specifically, I want the request to be executed every second in order to keep the data updated. While I know that using Obser ...

Prevent the element attribute from being enabled before the onclick function is triggered

I am attempting to implement a feature in Angular that prevents double clicking on buttons using directives. Below is the pertinent code snippet from my template: <button (click)="onClickFunction()" appPreventDoubleClick>Add</button> And her ...

Dynamically created HTML elements have no events attached to them in Angular and jQuery

Utilizing jQuery, I am dynamically generating new elements within an Angular Form that has been constructed using a Template Driven Forms approach. Despite successfully creating the dynamic elements, they do not seem to be assigned events/callbacks due to ...

When utilizing the package, an error occurs stating that Chart__default.default is not a constructor in chart.js

I have been working on a project that involves creating a package with a react chart component using chart.js. Everything runs smoothly when I debug the package in storybook. However, I encountered an error when bundling the package with rollup, referenc ...