Instantly share code, notes, and snippets.
Last active
September 13, 2024 00:01
-
Star
(7)
7
You must be signed in to star a gist -
Fork
(1)
1
You must be signed in to fork a gist
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <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