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

feat(client): ping from SharedWorker #19057

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
178 changes: 136 additions & 42 deletions packages/vite/src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,59 +323,153 @@ function hasErrorOverlay() {
return document.querySelectorAll(overlayId).length
}

async function waitForSuccessfulPing(socketUrl: string, ms = 1000) {
async function ping() {
const socket = new WebSocket(socketUrl, 'vite-ping')
return new Promise<boolean>((resolve) => {
function onOpen() {
resolve(true)
close()
}
function onError() {
resolve(false)
close()
}
function close() {
socket.removeEventListener('open', onOpen)
socket.removeEventListener('error', onError)
socket.close()
function waitForSuccessfulPing(socketUrl: string) {
// needs to be inlined to
// - load the worker after the server is closed
// - make it work with backend integrations
const blob = new Blob(
[
`const fn = ${pingWorkerContent.toString()};`,
`fn(${JSON.stringify(socketUrl)})`,
],
{ type: 'application/javascript' },
)
const objURL = URL.createObjectURL(blob)
const sharedWorker = new SharedWorker(objURL, { type: 'module' })
return new Promise<void>((resolve, reject) => {
const onVisibilityChange = () => {
sharedWorker.port.postMessage({ visibility: document.visibilityState })
}
document.addEventListener('visibilitychange', onVisibilityChange)

sharedWorker.port.addEventListener('message', (event) => {
document.removeEventListener('visibilitychange', onVisibilityChange)
sharedWorker.port.close()

const data: { type: 'success' } | { type: 'error'; error: unknown } =
event.data
if (data.type === 'error') {
reject(data.error)
return
}
socket.addEventListener('open', onOpen)
socket.addEventListener('error', onError)
resolve()
})
}

if (await ping()) {
return
onVisibilityChange()
sharedWorker.port.start()
})
}

function pingWorkerContent(socketUrl: string) {
type VisibilityManager = {
currentState: DocumentVisibilityState
listeners: Set<(newVisibility: DocumentVisibilityState) => void>
}
await wait(ms)

while (true) {
if (document.visibilityState === 'visible') {
if (await ping()) {
break
self.addEventListener('connect', (_event) => {
const event = _event as MessageEvent
const port = event.ports[0]

if (!socketUrl) {
port.postMessage({
type: 'error',
error: new Error('socketUrl not found'),
})
return
}

const visibilityManager: VisibilityManager = {
currentState: 'visible',
listeners: new Set(),
}
port.addEventListener('message', (event) => {
const { visibility } = event.data
visibilityManager.currentState = visibility
for (const listener of visibilityManager.listeners) {
listener(visibility)
}
await wait(ms)
} else {
await waitForWindowShow()
})
port.start()

console.debug('connected from window')
waitForSuccessfulPing(socketUrl, visibilityManager).then(
() => {
console.debug('ping successful')
try {
port.postMessage({ type: 'success' })
} catch (error) {
port.postMessage({ type: 'error', error })
}
},
(error) => {
console.debug('error happened', error)
try {
port.postMessage({ type: 'error', error })
} catch (error) {
port.postMessage({ type: 'error', error })
}
},
)
})

async function waitForSuccessfulPing(
socketUrl: string,
visibilityManager: VisibilityManager,
ms = 1000,
) {
async function ping() {
const socket = new WebSocket(socketUrl, 'vite-ping')
return new Promise<boolean>((resolve) => {
function onOpen() {
resolve(true)
close()
}
function onError() {
resolve(false)
close()
}
function close() {
socket.removeEventListener('open', onOpen)
socket.removeEventListener('error', onError)
socket.close()
}
socket.addEventListener('open', onOpen)
socket.addEventListener('error', onError)
})
}
}
}

function wait(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
if (await ping()) {
return
}
await wait(ms)

function waitForWindowShow() {
return new Promise<void>((resolve) => {
const onChange = async () => {
if (document.visibilityState === 'visible') {
resolve()
document.removeEventListener('visibilitychange', onChange)
while (true) {
if (visibilityManager.currentState === 'visible') {
if (await ping()) {
break
}
await wait(ms)
} else {
await waitForWindowShow(visibilityManager)
}
}
document.addEventListener('visibilitychange', onChange)
})
}

function wait(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms))
}

function waitForWindowShow(visibilityManager: VisibilityManager) {
return new Promise<void>((resolve) => {
const onChange = (newVisibility: DocumentVisibilityState) => {
if (newVisibility === 'visible') {
resolve()
visibilityManager.listeners.delete(onChange)
}
}
visibilityManager.listeners.add(onChange)
})
}
}

const sheetsMap = new Map<string, HTMLStyleElement>()
Expand Down
Loading