Skip to content

Instantly share code, notes, and snippets.

@Akxe
Last active December 2, 2024 14:31
Show Gist options
  • Save Akxe/b4cfefa0086f9a995a3578818af63ad9 to your computer and use it in GitHub Desktop.
Save Akxe/b4cfefa0086f9a995a3578818af63ad9 to your computer and use it in GitHub Desktop.
PortAwareSharedWorker, shared worker that know who is still connected and who is not
/// <reference lib="webworker" />
type SharedWorkerPort = MessagePort | DedicatedWorkerGlobalScope;
class PortAwareSharedWorkerPort<T extends SharedWorkerPort = SharedWorkerPort, D = any> {
private readonly weakRef: WeakRef<T>;
private disconnected = false;
constructor(
port: T,
onMessage: (eventData: D) => void,
private readonly onDisconnect: () => void,
onError?: (ev: MessageEvent<any>) => void,
) {
this.weakRef = new WeakRef(port);
port.onmessage = e => onMessage(e.data);
if (onError) {
port.onmessageerror = e => onError(e);
}
if ('start' in port) {
port.start();
}
}
isAlive(): boolean {
if (this.disconnected) {
// May occur, if the port was given away while alive, but response to it came after it "died"
return false;
} else if (!this.weakRef.deref()) {
// If port is no longer accessible, call destructor
this.onDisconnect();
this.disconnected = true;
return false
}
return true;
}
/**
* Posts a message through the channel. Objects listed in transfer are transferred, not just cloned, meaning that they are no longer usable on the sending side.
*
* Throws a "DataCloneError" DOMException if transfer contains duplicate objects or port, or if message could not be cloned.
*/
postMessage(message: any, transfer: Transferable[]): void;
postMessage(message: any, options?: StructuredSerializeOptions): void;
postMessage(message: any, options?: StructuredSerializeOptions | Transferable[]): void {
try {
const port = this.weakRef.deref();
if (!port) {
throw new TypeError(`Port is no longer reference-able`);
}
// In some browsers, if the other side of the port is no longer available, it will throw an error
port.postMessage(message, options as any);
} catch {
this.onDisconnect();
this.disconnected = true;
}
}
close() {
this.onDisconnect();
this.disconnected = true;
this.weakRef.deref()?.close();
}
}
export type { PortAwareSharedWorkerPort };
type SharedWorkerMessageHandler = (
responsiblePort: PortAwareSharedWorkerPort,
data: any,
allOpenedPorts: readonly PortAwareSharedWorkerPort[],
) => void;
type SharedWorkerDisconnectHandler = (
responsiblePort: PortAwareSharedWorkerPort,
allOpenedPorts: readonly PortAwareSharedWorkerPort[],
) => void;
export class PortAwareSharedWorker {
private readonly portsSet = new Set<PortAwareSharedWorkerPort>();
protected constructor(
port: SharedWorkerPort,
protected messageHandle: SharedWorkerMessageHandler,
protected disconnectHandle: SharedWorkerDisconnectHandler,
) {
this.initializePort(port);
// Poll based check to delete
setInterval(() => {
for (const port of this.portsSet) {
port.isAlive()
}
}, 100);
}
protected initializePort(port: SharedWorkerPort): void {
const portWrapper = new PortAwareSharedWorkerPort(
port,
data => this.messageHandle(portWrapper, data, this.getOpenPorts()),
() => this.disconnectHandle(
portWrapper,
this.getOpenPorts(),
),
);
this.portsSet.add(portWrapper);
}
/**
* Gets all currently opened ports. May also return some ports that are no longer active,
* but over time all inactive ports will be gone.
*
* @see(https://html.spec.whatwg.org/multipage/web-messaging.html#ports-and-garbage-collection)
*/
getOpenPorts(): readonly PortAwareSharedWorkerPort[] {
const remainingPorts: PortAwareSharedWorkerPort[] = [];
for (const port of this.portsSet) {
if (port.isAlive()) {
remainingPorts.push(port);
} else {
this.portsSet.delete(port);
}
}
return remainingPorts;
}
private static instance?: PortAwareSharedWorker;
static initializeProxy(
/** Pass `self` to this. The `self` can be from a `Worker` or `ServiceWorker` for convince */
global: any,
messageHandle: SharedWorkerMessageHandler,
disconnectHandle: SharedWorkerDisconnectHandler,
): Promise<PortAwareSharedWorker> {
return new Promise(resolve => {
if (PortAwareSharedWorker.instance) {
console.log('Returning worker singleton instead!');
return resolve(PortAwareSharedWorker.instance);
}
global.onconnect = function sharedConnectCallback(e: ExtendableMessageEvent) {
if (PortAwareSharedWorker.instance) {
PortAwareSharedWorker.instance.initializePort(e.ports[0]);
return;
}
resolve(PortAwareSharedWorker.instance = new PortAwareSharedWorker(e.ports[0], messageHandle, disconnectHandle));
}
// This is the fallback, just in case the browser doesn't support SharedWorkers
if (!('SharedWorkerGlobalScope' in global)) {
resolve(PortAwareSharedWorker.instance = new PortAwareSharedWorker(global as any, messageHandle, disconnectHandle));
}
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment