comlink icon indicating copy to clipboard operation
comlink copied to clipboard

Finalizer not invoked automatically with sharedworkers

Open lauri865 opened this issue 1 year ago • 3 comments

It doesn't seem like finalizer gets invoked for sharedworkers. I have the following worker, and the finalizer never gets invoked automatically.

declare const self: SharedWorkerGlobalScope;
import * as Comlink from "comlink";

export const api = {
  getToken: (token: string) => {
    return token;
  },
};

let id = 0;
self.addEventListener("connect", (event: MessageEvent): void => {
  id++;
  const tabId = id;
  console.log("tab connected", tabId);
  const port = event.ports[0];

  Comlink.expose(
    {
      ...api,
      [Comlink.finalizer]: () => {
        console.log("tab closed", tabId);
      },
    },
    port,
  );
});

It does work when I invoke it manually on beforeunload:

addEventListener("beforeunload", () => {
  instance[Comlink.releaseProxy]();
});

I also tried to wrap the port in weakRef myself before passing it to Comlink.expose, but no luck either. Was following this post: https://brightinventions.pl/blog/sharing-websocket-connections-between-browser-tabs-and-windows/

Environment: MacOS 15 Chrome Version 130.0.6723.117 (Official Build) (arm64)

lauri865 avatar Nov 10 '24 23:11 lauri865

I did manage to hack together a version with navigator.locks that seems to work.

Following this: https://github.com/whatwg/html/issues/1766#issuecomment-633197720

  • Acquire an infinite lock after Comlink.wrap with a random id
  • invoke "ready" function when the lock is acquired, which then tries to acquire the same lock
  • Lock gets released when the tab closes / crashes, we can then run a cleanup function for anything related to that tab

Bonus is that it should also be more performant than weakRef for ongoing tasks.

lauri865 avatar Nov 11 '24 00:11 lauri865

Oh interesting. That might be a corner-case (i.e. working as intended) of the FinalizationRegistry, where the finalizer does not get called when you close a tab. If you were to just drop the proxy, it should work. In general, FinalizationRegistry makes no guarantee about calling the finalizers.

So if you want to be sure that the finalizer gets called, your approach with beforeunload seems like a good idea.

surma avatar Nov 11 '24 08:11 surma

WeakRefs should work (in theory) when closing the tab according to some blog posts online, but I didn't get around to test yet outside the Comlink setup.

Unfortunately beforeunload is not reliable enough from our testing. Locks works great though.

For us, running a cleanup after tabs is crucial for memory management. The shared worker can be running for months on end with variable number of tabs.

Anyways, we're happy now, but there could be some merit in incorporating a solution into the library. So many people are asking the question, yet there's no straightforward solution.

lauri865 avatar Nov 11 '24 09:11 lauri865