Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: SharedWorker not working on some mobile browsers #336

Merged
merged 1 commit into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions src/socket/io.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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) => {
Expand Down
17 changes: 10 additions & 7 deletions src/socket/worker-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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',
})
}
Expand All @@ -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',
})
}
Expand Down
214 changes: 214 additions & 0 deletions src/socket/worker-polyfill.ts
Original file line number Diff line number Diff line change
@@ -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<K extends keyof WorkerEventMap>(
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<K extends keyof MessagePortEventMap>(
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<K extends keyof WorkerEventMap>(
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<K extends keyof MessagePortEventMap>(
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
Loading