mirror of
https://github.com/tuono-labs/tuono
synced 2026-07-25 12:52:47 -07:00
feat: add MessageChannel polyfill to support react 19 (#238)
This commit is contained in:
committed by
GitHub
parent
e99f06ddf5
commit
498601c21a
@@ -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'],
|
||||
}),
|
||||
},
|
||||
]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user