Detect when an observable is being subscribed to in RxJS

I am seeking a way to automatically subscribe to an observable (triggerEvent) when another observable (observedEvents) has been subscribed to. My goal is to activate triggerEvent without manual intervention, only once there is a subscription to observedEvents.

Below is a code snippet illustrating my requirements:

// This section emits events
let emitting = new EventEmitter();

// Main Observable that may be accessed by others
let observedEvents = Rx.Observable.merge(
  Rx.Observable.fromEvent(emitting, 'aba'),
  Rx.Observable.fromEvent(emitting, 'bob')
)

// I want this trigger to get a subscription as soon as 
// observedEvents has one, meaning when I subscribe to 
// observedEvents, it activates this trigger

// I tried using skipUntil, but instead of skipping one event,
// I'm looking for continuous activation
let triggerEvent = Rx.Observable.merge(
  Rx.Observable.of('a').delay(200),
  Rx.Observable.of('b').delay(400),
  Rx.Observable.of('c').delay(600)
).skipUntil(observedEvents);

// Something should trigger the activation of triggerEvent
// without manual intervention
triggerEvent.subscribe(val => {
    console.log(`Perform action with ${val}`);
});

//----------------------------------------------------
// Somewhere else in the code...
//----------------------------------------------------
observedEvents.subscribe(evt => {
  console.log(`Received event: ${evt}`);
});
// At this point, triggerEvent should become active
// because observedEvents now has a subscription

setTimeout(() => {
  emitting.emit('bob', 'world');
  setTimeout(() => emitting.emit('aba', 'stackoverflow!'), 500);
}, 200);
<!DOCTYPE html>
<html>
<head>
  <script src="https://npmcdn.com/@reactivex/<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="0e7c76647d4e3b203e203e236c6b7a6f2039">[email protected]</a>/dist/global/Rx.umd.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/EventEmitter/5.1.0/EventEmitter.min.js"></script>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>

</body>
</html>

Is this approach feasible?

I trust this clarifies my requirement.

Reflecting on this scenario, utilizing Subjects appears to be the potential solution. Perhaps a gentle push in the right direction or a viable solution would be greatly appreciated.

Answer №1

If you are using rxjs version greater than 7, refer to this particular response


Solution

Indeed, my decision to utilize Subjects was correct. The crucial element turned out to be the list of observers for Subject. Here is the final approach I implemented:

let emitting = new EventEmitter();
let sub = new Rx.Subject();

// Expose this to users
let myGlobalSub = sub.merge(Rx.Observable.of(1, 2, 3));

// Used internally
let myObservers = Rx.Observable.fromEvent(emitting, 'evt');

console.log(`The number of subscribers is ${sub.observers.length}`);

// Only take action if myGlobalSub has subscribers
myObservers.subscribe(l => {
  if (sub.observers.length) { // checking observers here
    console.log(l);
  }
});

// Somewhere in the code...
emitting.emit('evt', "I don't want to see this"); // No output because no subscribers

myGlobalSub.subscribe(l => console.log(l)); // One sub

emitting.emit('evt', 'I want to see this'); // Output due to one subscriber

console.log(`The number of subscribers is ${sub.observers.length}`);
<!DOCTYPE html>
<html lang=en>

<head>
  <script src="https://unpkg.com/<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="ef9d97859cafdac1dac1dede">[email protected]</a>/bundles/Rx.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/EventEmitter/5.2.5/EventEmitter.min.js"></script>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>

<body>
</body>

</html>

Answer №2

@smac89's response mentions the use of the observers property, which has been deprecated in RxJS 7 and is set to be removed in version 8.

If you simply want to determine if an Observable has been subscribed to, you can utilize the boolean property observed instead.

As of now, there isn't a direct replacement for the observers array.

let emitting = new EventEmitter();
let sub = new rxjs.Subject();

// Return this to users
let myGlobalSub = rxjs.merge(sub,rxjs.of(1, 2, 3));

// For internal use
let myObservers = rxjs.fromEvent(emitting, 'evt');

console.log(`Has the subject been subscribed to? ${sub.observed}`);

// Only perform action if myGlobalSub has subscribers
myObservers.subscribe(l => {
  if (sub.observed) { // here we check observers
    console.log(l);
  }
});

// Somewhere in the code...
emitting.emit('evt', "I don't want to see this"); // No output because no subscribers

myGlobalSub.subscribe(l => console.log(l)); // One sub

emitting.emit('evt', 'I want to see this'); // Output because of one sub

console.log(`Has the subject been subscribed to? ${sub.observed}`);
<!DOCTYPE html>
<html lang=en>

<head>
  <script src="https://unpkg.com/rxjs@%5E7/dist/bundles/rxjs.umd.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/EventEmitter/5.2.5/EventEmitter.min.js"></script>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>

<body>
</body>

</html>

Answer №3

UPDATE: It turns out that tap already has built-in support for this functionality without the need for additional objects.

Instead of creating a complex web of interconnected objects like in the accepted solution, we can achieve the same result by writing our own operator function that exclusively utilizes existing rxjs operators! In my scenario, I was searching for a combination of startWith and tap; triggering a side effect upon subscription, without concern for the specifics of the action or its return value.

If given a source observable, here's how we can accomplish it:

const source = of(1,2,3).pipe(
  onSubscription(() => /* some action */ console.log('observable subscribed')),
  /* add your code here */
  tap({
    next: n => console.log('next ', n),
    error: e => console.log('error ', e),
    complete: () => console.log('observable complete')
  }),
);

If you refer to the guide on creating custom operators, underneath all the details is a simple concept: an operator is basically a function that takes something and returns a function that operates on an observable (your source observable) and produces another observable.

Therefore, the initial step of our operator should be just passing along the source observable since we're not intending to alter it.

onSubscription<T>() {
  return (obs: Observable<T>) => {
    return obs;
  };
}

Next, we need to determine how to integrate it with our callback function.

// INCORRECT
onSubscription<T>(callback: Function) {
  return (obs: Observable<T>) => {
    callback();
    return obs;
  };
}

This approach has an issue. If you don't subscribe to source at this point, you'll still see "observable subscribed" in the output. The problem lies in the fact that callback() runs during the setup phase before the actual subscription occurs. We need to incorporate it into the observable stream while ensuring it executes immediately. Since of triggers actions right away, we can use map to invoke our callback off of that (note that map must be used instead of tap to avoid including null from of in the final output type), followed by combining it with our source observable using merge!

onSubscription<T>(callback: Function) {
  return (obs: Observable<T>) => {
    // Including null (or any value) to activate the tap
    return merge(of(null).pipe(map(() => callback())), obs);
  };
}

Now, if you don't subscribe to source, there will be no output until you do....but the output will also contain the null from of. To resolve this, we can have merge skip the first emitted value, completing the process!

onSubscription<T>(callback: Function) {
  return (obs: Observable<T>) => {
    return merge(of(null).pipe(map(() => callback())), obs).pipe(skip(1));
  };
}

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

Implementing a restriction clause while executing a load additional data feature

I'm currently working on implementing a basic load more function for my web page. Right now, the page displays 8 results from the database. When the user scrolls, a load more button appears. Clicking this button triggers an ajax request to another PH ...

Accessing information from database using Javascript

I am facing a dilemma with my HTML and PHP pages. The HTML page contains a table, while the PHP page executes a query to a MySQL table. I am struggling to display the query results on my HTML page. The HTML page features the following code snippet: <t ...

Utilize Three.js to Add Depth to 2D Images with Extrusion

After searching online, I have been unable to find a method to extrude a colored image in Three.js. My goal is to create a design similar to a Minecraft item where the image is used to generate an extruded geometry. An example of what I'm trying to ac ...

If you press Ctrl + F, the browser will disable the form search function

How can I prevent the form find browser functionality when pressing Ctrl+F, and instead focus on the element html? <div id='demo'> <form class="id5-text-find-form" id="id5-text-find-form"> <input class="search" placeho ...

Can you explain the purpose of useEffect in React?

As a beginner in learning React, I have been able to grasp the concept of the useState hook quite well. However, I am facing some difficulties understanding the useEffect hook. I have tried looking through the documentation, searching online, and even wat ...

I am looking to transfer an image stored in a database onto a card

One issue I am facing is that when trying to display an image from a database, uploaded by a user and showing on a card with additional information like name and price, an icon resembling a paper with green appears in the output. Here's my code snippe ...

The object with the tag HTMLAnchorElement is unable to access the 'attr' method

I need to add the URL from the browser before each menu item due to some URL redirect issues. However, I am encountering an error in the code snippet below. What am I doing incorrectly? What is the correct approach? $(window).load(function () { ...

What is the best way to monitor React hooks efficiently?

Prior to diving into a new React project, I always make sure that there are adequate developer tools available for support. One of my favorite features in React is the React Developer tool for Google Chrome. It allows me to examine the internal state of e ...

What is the method for ensuring a variable returns a number and not a function?

I've encountered a perplexing issue that I can't seem to solve. What could be causing the code below to output a function instead of a number? let second = function(){ return 100 }; console.log(second); ...

Objects That Are Interconnected in Typescript

I have been delving into TS and Angular. I initially attempted to update my parent component array with an EventEmitter call. However, I later realized that this was unnecessary because my parent array is linked to my component variables (or so I believe, ...

`Async/await: Implementing a timeout after a fadeout once the fadein operation is

Here is a snippet of code that demonstrates the process: async function loadForm (data) { let promiseForm = pForm(data); await promiseForm.then(function(data) { setTimeout(function () { $container.fadeOut(function() { ...

Using the md-date-picker along with the md-menu component

Whenever I try to click on the md-date-picker inside md-menu, it unexpectedly closes. You can view the issue on this CodePen link. This seems to be a bug related to ng-material as discussed in this GitHub issue. Any suggestions for a workaround? Here is t ...

Fetching the Three.js mesh from the loader outside the designated area

Currently, I am immersed in a project that involves Three.js. The challenge I am facing is trying to access the mesh (gltf.scene) in the GLTF load from an external source. However, whenever I attempt to console.log outside of the loader, it shows up as und ...

Utilize the Multer file upload feature by integrating it into its own dedicated controller function

In my Express application, I decided to keep my routes.js file organized by creating a separate UploadController. Here's what it looks like: // UploadController.js const multer = require('multer') const storage = multer.diskStorage({ dest ...

Fill in a URL using the information provided in the boxes and navigate to the website

My colleagues and I access the following link multiple times a day, clicking through several pages to reach this specific URL: http://eharita.mamak.bel.tr/imararsiv/test.aspx?f_ada=36391&f_parsel=4 I am looking to create an HTML page with two input b ...

What are the steps to limit the usage of angular.module('myApp', ['ngGrid', 'ui.bootstrap', 'kendo.directives'])?

I have two JavaScript files with AngularJS code. The first file, myjs1.js, includes the module: angular.module('myApp', [ 'ngGrid', 'ui.bootstrap', 'kendo.directives' ]); The second file, myjs2.js, also includes th ...

When initiating the Grunt Express Server, it prompts an issue: Error: ENOENT - the file or directory 'static/test.json' cannot be found

I'm currently in the process of updating my app to utilize the Express Node.js library. As part of this update, I have made changes to my Grunt.js tasks to incorporate the grunt-express-server package. However, after running the server successfully, I ...

Using JavaScript within Razor C#

I am attempting to invoke a JavaScript function from within a helper method in Razor. Here is a snippet of my code: @helper MyMethod() { for (int i = 0; i < 5; i++) { drawMe(i) } } The drawMe function is defined in an externa ...

How to Efficiently Organize OpenAI AI Responses Using TypeScript/JavaScript and CSS

I'm using a Next.js framework to connect to the OpenAI API, and I've integrated it seamlessly with an AI npm package. The functionality is incredible, but I've encountered an issue regarding line breaks in the responses. You can find the AI ...

Deciphering JSON data within AngularJS

When I retrieve JSON data in my controller using $http.get, it looks like this: $http.get('http://webapp-app4car.rhcloud.com/product/feed.json').success(function(data) The retrieved data is in JSON format and I need to access the value correspo ...