'Are event listeners from an EventSource automatically garbage collected?

I've seen answers on Stackoverflow mentioning how if a DOM element goes out of scope, its event listeners are automatically garbage collected. Is that also the case with non-DOM elements, such as EventSource?

Here's an example of what I mean:

function checkStatus(events) {
   return function() {
     if (events.readyState === EventSource.CLOSED) { 
       establishSSE(); // Is this fine?
     } else {
       setTimeout(checkStatus(events), 500);
     }
   }
}

function establishSSE() {
  const events = new EventSource("/sse/" + sseID);

  events.addEventListener("reply", sseReply);
  events.addEventListener("refresh", sseRefresh);

  setTimeout(checkStatus(events), 500);
}

establishSSE();

(I know I can use the onerror event. This is just an example)

In this case, would the previous event handlers be garbage collected when establishSSE was re-ran on line 4?


Edit:

I don't understand how these values can be garbage collected. For example, the following works:

function sseSubscribe() {
  let events = new EventSource("/v1/sse/mySSEID");

  events.addEventListener("message", sseHeartbeat);
  events.addEventListener("reply", sseReply);
}

That is, new messages still cause the message and reply handlers to run.

But events is no longer accessible from anywhere within the program. So, shouldn't it have been garbage collected? It seems like EventSource is never garbage collected (unless maybe the connection is broken?)


Edit #2:

It seems that even if the event handlers are anonymous, like so:

function sseSubscribe() {
  let events = new EventSource("/v1/sse/SSEID");

  events.addEventListener("message", function() {
    console.log("Heartbeat");
  });
 }

 sseSubscribe();

This still works on my end. That is, Heartbeat is being logged into the console every 30 seconds (the interval my server sends them). This is despite events immediately going out of scope and being inaccessible anywhere else in the program.



Solution 1:[1]

Answer to question title

Yes and no (from comment). The event source value can be garbage collected when it can't be accessed anywhere1. When and if it is garbage collected, any anonymous event handler functions declared as inline arguments to addEventListener will be garbage collected. Functions defined more globally that can still be reached can't be collected.

  • JavaScript values become eligible for garbage collection when they can't be reached in code.

  • The event listener functions of an events value are recorded in a map of event listeners of the object: if the object can't be reached in code, the event listeners can't be reached either. If the only way or reaching them was through the event handler map (they were anonymous) the can be garbage collected.

Each time establishSSE is called it returns a new EventSource object - which, as an object "value", is different than the one returned the previous time it was called.

When passed to checkStatus, the value of events is held in a closure of the function passed to setTimeout. Hence the value will be "reached" when the timeout expires and the timer function executed.

One of two cases occur next

  1. The EventSource object in events is still open. A new timeout is created by calling checkStatus again. This creates a new closure for a new timer callback function, but with the same value of the events argument. Hence the (unchanged) value of events can still be reached and can't be garbage collected.

  2. The event source in events has been closed. In this case checkStatus calls establishSSE (to reopen the source) and returns from the timer call without storing its argument anywhere or retaining it in the scope of a closure that can be executed at a later time. The closed events object becomes eligible for garbage collection at this point because it can't be reached.

    establishSSE goes on to create a new value held in a variable called events, and calls checkStatus with it, but it's not the same value as the one going out of scope in the checkStatus call above it in the call stack.

However, since sseReply and sseRefresh are not nested within estabishSSE, they won't be garbage collected just because an event object value was gc'ed.


1 With an exception: blob or file object arguments passed to createObjectURL are held in memory (while network traffic retrieves their data) and must be explicitly released by calling URL.revokeObjectURL.


Answer to Edits

Updated after question edit

Butevents is no longer accessible from anywhere within the program.

This is an invalid assumption which may have lead to the question:

  1. The events argument of checkStatus is in scope of the timer callback function returned by checkStatus because the callback function is nested within checkStatus.

  2. The callback function is prevented from being garbage collected because the [native code] implementation of setTimeout necessarily holds a reference to it.

  3. If the value of events is not in a closed state, the timer callback creates a new timer callback function by making a fresh call tocheckStatus using the existing events object value, to generate a new callback function for a new setTimeout call.

  4. Repeat from step 1.

It might be more straight forward and less confusing if checkStatus were used as the timer callback directly without making a delayed recursive call to itself, say by passing arguments via setTimeout:

function checkStatus(events) {
    if (events.readyState === EventSource.CLOSED) { 
          establishSSE(); // create a new EventSource object
          return; // the actual argument object value is now inaccessible
                  // and becomes eligble for garbage collection.
    }
    setTimeout( checkStatus, 500, events);
}

If implementing the change above, establishSS would also need to pass checkStatus to setTimeout as its first argument without calling it first:

function establishSSE() {
  const events = new EventSource("/sse/" + sseID);

  events.addEventListener("reply", sseReply);
  events.addEventListener("refresh", sseRefresh);

  setTimeout(checkStatus, 500, events);
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1