From c549a2fcbe38ab701bcb55c9ce5a4536fb720778 Mon Sep 17 00:00:00 2001 From: Launchpad Date: Wed, 10 Apr 2024 10:45:35 +0800 Subject: [PATCH] fix: SharedWorker not working on some mobile browsers (#336) --- src/socket/io.worker.ts | 27 +++-- src/socket/worker-client.ts | 17 +-- src/socket/worker-polyfill.ts | 214 ++++++++++++++++++++++++++++++++++ 3 files changed, 243 insertions(+), 15 deletions(-) create mode 100644 src/socket/worker-polyfill.ts diff --git a/src/socket/io.worker.ts b/src/socket/io.worker.ts index 4a09795df7..f9bf161be1 100644 --- a/src/socket/io.worker.ts +++ b/src/socket/io.worker.ts @@ -7,7 +7,7 @@ import type { Socket } from 'socket.io-client' let ws: Socket | null = null -function setupIo(config: { url: string }) { +function setupIo(config: { url: string; socket_session_id: string }) { if (ws) return // 使用 socket.io console.log('Connecting to io, url: ', config.url) @@ -18,6 +18,10 @@ function setupIo(config: { url: string }) { autoConnect: false, reconnectionAttempts: 3, transports: ['websocket'], + + query: { + socket_session_id: config.socket_session_id, + }, }) if (!ws) return @@ -65,13 +69,7 @@ function setupIo(config: { url: string }) { const ports = [] as MessagePort[] -self.addEventListener('connect', (ev: any) => { - const event = ev as MessageEvent - - const port = event.ports[0] - - ports.push(port) - +const preparePort = (port: MessagePort | Window) => { port.onmessage = (event) => { const { type, payload } = event.data console.log('get message from main', event.data) @@ -101,10 +99,23 @@ self.addEventListener('connect', (ev: any) => { console.log('Unknown message type:', type) } } +} +self.addEventListener('connect', (ev: any) => { + const event = ev as MessageEvent + + const port = event.ports[0] + + ports.push(port) + preparePort(port) port.start() }) +if (!('SharedWorkerGlobalScope' in self)) { + ports.push(self as any as MessagePort) + preparePort(self) +} + function boardcast(payload: any) { console.log('[ws] boardcast', payload) ports.forEach((port) => { diff --git a/src/socket/worker-client.ts b/src/socket/worker-client.ts index 9672f8beff..f9babf7dd3 100644 --- a/src/socket/worker-client.ts +++ b/src/socket/worker-client.ts @@ -4,12 +4,14 @@ import type { AppRouterInstance } from 'next/dist/shared/lib/app-router-context. import { simpleCamelcaseKeys as camelcaseKeys } from '@mx-space/api-client' +import { getSocketWebSessionId } from '~/atoms/hooks' import { setSocketIsConnect } from '~/atoms/socket' import { GATEWAY_URL } from '~/constants/env' import { SocketConnectedEvent, SocketDisconnectedEvent } from '~/events' import { isDev, isServerSide } from '~/lib/env' import { eventHandler } from './handler' +import { SharedWorkerPolyfill as SharedWorker } from './worker-polyfill' interface WorkerSocket { sid: string @@ -48,13 +50,13 @@ class SocketWorker { } } bindMessageHandler = (worker: SharedWorker) => { - worker.port.onmessage = (event: MessageEvent) => { + worker.onmessage = (event: MessageEvent) => { const { data } = event const { type, payload } = data switch (type) { case 'ping': { - worker?.port.postMessage({ + worker?.postMessage({ type: 'pong', }) console.log('[ws worker] pong') @@ -99,17 +101,18 @@ class SocketWorker { prepare(worker: SharedWorker) { const gatewayUrlWithoutTrailingSlash = GATEWAY_URL.replace(/\/$/, '') this.bindMessageHandler(worker) - worker.port.postMessage({ + worker.postMessage({ type: 'config', payload: { url: `${gatewayUrlWithoutTrailingSlash}/web`, + socket_session_id: getSocketWebSessionId(), }, }) - worker.port.start() + worker.start() - worker.port.postMessage({ + worker.postMessage({ type: 'init', }) } @@ -125,14 +128,14 @@ class SocketWorker { } emit(event: SocketEmitEnum, payload: any) { - this.worker?.port.postMessage({ + this.worker?.postMessage({ type: 'emit', payload: { type: event, payload }, }) } reconnect() { - this.worker?.port.postMessage({ + this.worker?.postMessage({ type: 'reconnect', }) } diff --git a/src/socket/worker-polyfill.ts b/src/socket/worker-polyfill.ts new file mode 100644 index 0000000000..a5002d2b4e --- /dev/null +++ b/src/socket/worker-polyfill.ts @@ -0,0 +1,214 @@ +// Copy from https://github.com/okikio/sharedworker/blob/31830ea0f1f4b1d1cf1444aee7fb1ffd832f63e3/src/index.ts, which is licensed under the MIT license. + +// Adapted from https://github.com/okikio/bundle/blob/main/src/ts/util/WebWorker.ts, which is licensed under the MIT license. +// If the above file is removed or modified, you can access the original state in the following GitHub Gist: https://gist.github.com/okikio/6809cfc0cdbf1df4c0573addaaf7e259 + +/** + * A polyfill class for `SharedWorker`, it accepts a URL/string as well as any other options the spec. allows for `SharedWorker`. It supports all the same methods and properties as the original, except it adds compatibility methods and properties for older browsers that don't support `SharedWorker`, so, it can switch to normal `Workers` instead. + */ +export class SharedWorkerPolyfill + implements SharedWorker, EventTarget, AbstractWorker +{ + /** + * The actual worker that is used, depending on browser support it can be either a `SharedWorker` or a normal `Worker`. + */ + public ActualWorker: SharedWorker | Worker + constructor(url: string | URL, opts?: WorkerOptions) { + if ('SharedWorker' in window) { + this.ActualWorker = new SharedWorker(url, opts) + } else { + this.ActualWorker = new Worker(url, opts) + } + } + + /** + * An EventListener called when MessageEvent of type message is fired on the port—that is, when the port receives a message. + */ + public get onmessage() { + if ('SharedWorker' in window) { + return (this.ActualWorker as SharedWorker)?.port.onmessage + } else { + return (this.ActualWorker as Worker) + .onmessage as unknown as MessagePort['onmessage'] + } + } + + public set onmessage(value: MessagePort['onmessage'] | Worker['onmessage']) { + if ('SharedWorker' in window) { + ;(this.ActualWorker as SharedWorker).port.onmessage = + value as MessagePort['onmessage'] + } else { + ;(this.ActualWorker as Worker).onmessage = value as Worker['onmessage'] + } + } + + /** + * An EventListener called when a MessageEvent of type MessageError is fired—that is, when it receives a message that cannot be deserialized. + */ + public get onmessageerror() { + if ('SharedWorker' in window) { + return (this.ActualWorker as SharedWorker)?.port.onmessageerror + } else { + return (this.ActualWorker as Worker).onmessageerror + } + } + + public set onmessageerror( + value: MessagePort['onmessageerror'] | Worker['onmessageerror'], + ) { + if ('SharedWorker' in window) { + ;(this.ActualWorker as SharedWorker).port.onmessageerror = + value as MessagePort['onmessageerror'] + } else { + ;(this.ActualWorker as Worker).onmessageerror = + value as Worker['onmessageerror'] + } + } + + /** + * Starts the sending of messages queued on the port (only needed when using EventTarget.addEventListener; it is implied when using MessagePort.onmessage.) + */ + public start() { + if ('SharedWorker' in window) { + return (this.ActualWorker as SharedWorker)?.port.start() + } + } + + /** + * Clones message and transmits it to worker's global environment. transfer can be passed as a list of objects that are to be transferred rather than cloned. + */ + public postMessage( + message: any, + transfer?: Transferable[] | StructuredSerializeOptions, + ) { + if ('SharedWorker' in window) { + return (this.ActualWorker as SharedWorker)?.port.postMessage( + message, + transfer as Transferable[], + ) + } else { + return (this.ActualWorker as Worker).postMessage( + message, + transfer as Transferable[], + ) + } + } + + /** + * Immediately terminates the worker. This does not let worker finish its operations; it is halted at once. ServiceWorker instances do not support this method. + */ + public terminate() { + if ('SharedWorker' in window) { + return (this.ActualWorker as SharedWorker)?.port.close() + } else { + return (this.ActualWorker as Worker).terminate() + } + } + + /** + * Disconnects the port, so it is no longer active. + */ + public close() { + return this.terminate() + } + + /** + * Returns a MessagePort object used to communicate with and control the shared worker. + */ + public get port() { + return ( + 'SharedWorker' in window + ? (this.ActualWorker as SharedWorker).port + : this.ActualWorker + ) as MessagePort + } + + /** + * Is an EventListener that is called whenever an ErrorEvent of type error event occurs. + */ + public get onerror() { + return this.ActualWorker.onerror + } + public set onerror( + value: ((this: AbstractWorker, ev: ErrorEvent) => any) | null, + ) { + this.ActualWorker.onerror = value + } + + /** + * Registers an event handler of a specific event type on the EventTarget + */ + public addEventListener( + type: K, + listener: (this: Worker, ev: WorkerEventMap[K]) => any, + options?: boolean | AddEventListenerOptions, + ): void + public addEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions, + ): void + public addEventListener( + type: K, + listener: (this: MessagePort, ev: MessagePortEventMap[K]) => any, + options?: boolean | AddEventListenerOptions, + ): void + public addEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions, + ): void { + if ('SharedWorker' in window && type !== 'error') { + return (this.ActualWorker as SharedWorker)?.port.addEventListener( + type, + listener, + options, + ) + } else { + return this.ActualWorker.addEventListener(type, listener, options) + } + } + + /** + * Removes an event listener from the EventTarget. + */ + public removeEventListener( + type: K, + listener: (this: Worker, ev: WorkerEventMap[K]) => any, + options?: boolean | EventListenerOptions, + ): void + public removeEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions, + ): void + public removeEventListener( + type: K, + listener: (this: MessagePort, ev: MessagePortEventMap[K]) => any, + options?: boolean | EventListenerOptions, + ): void + public removeEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions, + ): void { + if ('SharedWorker' in window && type !== 'error') { + return (this.ActualWorker as SharedWorker)?.port.removeEventListener( + type, + listener, + options, + ) + } else { + return this.ActualWorker.removeEventListener(type, listener, options) + } + } + + /** + * Dispatches an event to this EventTarget. + */ + public dispatchEvent(event: Event) { + return this.ActualWorker.dispatchEvent(event) + } +} + +export default SharedWorkerPolyfill