feat: add MessageChannel polyfill to support react 19 (#238)

This commit is contained in:
Mustafa Zaki Assagaf
2024-12-21 22:55:04 +07:00
committed by GitHub
parent e99f06ddf5
commit 498601c21a
4 changed files with 245 additions and 0 deletions
+6
View File
@@ -15,6 +15,12 @@ const VITE_SSR_PLUGINS: Array<Plugin> = [
enforce: 'post',
...inject({
ReadableStream: ['web-streams-polyfill', 'ReadableStream'],
/**
* Added to support `react@19`
* @see https://github.com/tuono-labs/tuono/issues/218
*/
MessageChannel: ['tuono/ssr', 'MessageChannelPolyfill'],
}),
},
]
+3
View File
@@ -8,6 +8,7 @@ import { HelmetProvider } from 'react-helmet-async'
import { RouterProvider, createRouter } from 'tuono-router'
import type { createRoute } from 'tuono-router'
import { MessageChannelPolyfill } from './polyfills/messageChannel'
import { streamToString } from './utils'
type RouteTree = ReturnType<typeof createRoute>
@@ -94,3 +95,5 @@ export function serverSideRendering(routeTree: RouteTree) {
`
}
}
export { MessageChannelPolyfill }
@@ -0,0 +1,140 @@
import { describe, it, expect } from 'vitest'
import { MessageChannelPolyfill, MessagePortPolyfill } from './messageChannel'
describe('MessagePortPolyfill', () => {
it('should invoke onmessage when a message is posted', () => {
const port = new MessagePortPolyfill()
let messageReceived: string | null = null
port.onmessage = (event: MessageEvent<string>): void => {
messageReceived = event.data
}
port.dispatchEvent({ data: 'Hello, world!' } as MessageEvent)
expect(messageReceived).toBe('Hello, world!')
})
it('should handle multiple event listeners', () => {
const port = new MessagePortPolyfill()
const messages: Array<string> = []
const listener1 = ((event: MessageEvent): void => {
messages.push('Listener1: ' + event.data)
}) as EventListener
const listener2 = ((event: MessageEvent): void => {
messages.push('Listener2: ' + event.data)
}) as EventListener
port.addEventListener('message', listener1)
port.addEventListener('message', listener2)
port.dispatchEvent({ data: 'Test message' } as MessageEvent)
expect(messages).toEqual([
'Listener1: Test message',
'Listener2: Test message',
])
})
it('should not invoke removed event listeners', () => {
const port = new MessagePortPolyfill()
const messages: Array<string> = []
const listener = ((event: MessageEvent<string>): void => {
messages.push(event.data)
}) as EventListener
port.addEventListener('message', listener)
port.dispatchEvent({ data: 'First message' } as MessageEvent)
port.removeEventListener('message', listener)
port.dispatchEvent({ data: 'Second message' } as MessageEvent)
expect(messages).toEqual(['First message'])
})
it('should not post messages if otherPort is null', () => {
const port = new MessagePortPolyfill()
let messageReceived: string | null = null
port.onmessage = (event: MessageEvent<string>): void => {
messageReceived = event.data
}
port.postMessage('Hello!')
expect(messageReceived).toBeNull()
})
})
describe('MessageChannelPolyfill', () => {
it('should send and receive messages between ports', () => {
const channel = new MessageChannelPolyfill()
const messages: Array<string> = []
channel.port1.onmessage = (event: MessageEvent<string>): void => {
messages.push(event.data)
}
channel.port2.postMessage('Hello, port1!')
channel.port2.postMessage('How are you?')
expect(messages).toEqual(['Hello, port1!', 'How are you?'])
})
it('should support addEventListener and removeEventListener', () => {
const channel = new MessageChannelPolyfill()
const messages: Array<string> = []
const listener = ((event: MessageEvent<string>): void => {
messages.push(event.data)
}) as EventListener
channel.port1.addEventListener('message', listener)
channel.port2.postMessage('Hello, port1!')
expect(messages).toEqual(['Hello, port1!'])
channel.port1.removeEventListener('message', listener)
channel.port2.postMessage('Message after removing listener')
expect(messages).toEqual(['Hello, port1!'])
})
it('should handle bidirectional communication between ports', () => {
const channel = new MessageChannelPolyfill()
const messagesPort1: Array<string> = []
const messagesPort2: Array<string> = []
channel.port1.onmessage = (event: MessageEvent<string>): void => {
messagesPort1.push(event.data)
}
channel.port2.onmessage = (event: MessageEvent<string>): void => {
messagesPort2.push(event.data)
}
channel.port1.postMessage('Hello, port2!')
channel.port2.postMessage('Hello, port1!')
expect(messagesPort1).toEqual(['Hello, port1!'])
expect(messagesPort2).toEqual(['Hello, port2!'])
})
})
describe('MessagePort', () => {
it('should not send a message on close', () => {
const { port1, port2 } = new MessageChannelPolyfill()
const messages: Array<string> = []
port1.onmessage = (event: MessageEvent<string>): void => {
messages.push(event.data)
}
port2.postMessage('Test message')
expect(messages).toEqual(['Test message'])
port1.close()
port2.postMessage('Another message')
expect(messages).toEqual(['Test message'])
})
})
@@ -0,0 +1,96 @@
/* Modified from https://github.com/rocwind/message-port-polyfill/blob/master/src/index.ts
* MIT License
*
* Copyright (c) 2019 Roc
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
export class MessagePortPolyfill implements MessagePort {
onmessage: ((this: MessagePort, ev: MessageEvent) => unknown) | null = null
onmessageerror: ((this: MessagePort, ev: MessageEvent) => unknown) | null =
null
otherPort: MessagePortPolyfill | null = null
private onmessageListeners: Array<(ev: MessageEvent) => void> = []
private isClosed = false
dispatchEvent(event: MessageEvent): boolean {
if (this.isClosed) return false
if (this.onmessage) {
this.onmessage(event)
}
this.onmessageListeners.forEach((listener) => listener(event))
return true
}
postMessage(message: unknown): void {
if (this.isClosed || !this.otherPort) return
const event = new MessageEvent('message', { data: message })
this.otherPort.dispatchEvent(event)
}
addEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
): void {
if (this.isClosed || type !== 'message') return
if (
typeof listener === 'function' &&
!this.onmessageListeners.includes(listener)
) {
this.onmessageListeners.push(listener as (ev: MessageEvent) => void)
}
}
removeEventListener(
type: string,
listener: EventListenerOrEventListenerObject,
): void {
if (this.isClosed || type !== 'message') return
if (typeof listener === 'function') {
const index = this.onmessageListeners.indexOf(
listener as (ev: MessageEvent) => void,
)
if (index !== -1) {
this.onmessageListeners.splice(index, 1)
}
}
}
start(): void {
// do nothing at this moment
}
close(): void {
this.isClosed = true
}
}
export class MessageChannelPolyfill implements MessageChannel {
readonly port1: MessagePortPolyfill
readonly port2: MessagePortPolyfill
constructor() {
this.port1 = new MessagePortPolyfill()
this.port2 = new MessagePortPolyfill()
this.port1.otherPort = this.port2
this.port2.otherPort = this.port1
}
}