Skip to content
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
26 changes: 21 additions & 5 deletions packages/web-worker/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,32 @@ function createClonedMessageEvent(
debug('clone worker message %o', data)
const origin = typeof location === 'undefined' ? undefined : location.origin
const ports = transfer?.filter((t): t is MessagePort => t instanceof MessagePort)
const transferWithoutPorts = transfer?.filter( // `ports` must be excluded from the `transfer` option passed to `structuredClone` to keep the MessagePort objects working correctly in the same thread.
t => !(t instanceof MessagePort),
)

if (typeof structuredClone === 'function' && clone === 'native') {
debug('create message event, using native structured clone')
// A real Worker serializes `data` across realms and exposes the
// MessagePorts from `transfer` as `event.ports` on the receiving side.
// @vitest/web-worker runs both sides in a single realm, so we use
// `structuredClone` to emulate that transfer boundary.
//
// `MessageEvent.ports` must be the *cloned* ports returned by
// `structuredClone`, not the originals from `transfer`: once transferred,
// the originals are detached and can no longer communicate — e.g.
// `port1.postMessage(...)` on the caller side would not trigger
// `port2.onmessage` on a detached `port2`.
//
// `data` and `ports` must also be cloned in the *same* `structuredClone`
// call. A transferred object is detached immediately, so we cannot clone
// `data` first and then clone `ports` (or vice versa) — the second call
// would see already-detached ports. Passing them together as a single
// input also makes `structuredClone` deduplicate by identity, so a port
// referenced from inside `data` and from `transfer` resolves to the same
// transferred instance in the cloned graph.
const { data: clonedData, ports: clonedPorts } = structuredClone({ data, ports }, { transfer })
return new MessageEvent('message', {
data: structuredClone(data, { transfer: transferWithoutPorts }),
data: clonedData,
origin,
ports,
ports: clonedPorts,
})
}
if (clone !== 'none') {
Expand Down
5 changes: 5 additions & 0 deletions test/core/src/web-worker/worker.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
self.onmessage = (e) => {
self.postMessage(`${e.data} world`)

const portPassedAsData = e.data?.port
if (portPassedAsData) {
portPassedAsData.postMessage(`Reply via port in data`)
}

const port = e.ports[0]
if (port) {
port.postMessage(`${e.data} world via port`)
Expand Down
16 changes: 15 additions & 1 deletion test/core/test/web-worker-node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ cloneTypes.forEach((clone) => {
process.env.VITEST_WEB_WORKER_CLONE = undefined
})

it('transfers MessagePort objects to worker as event.ports', async () => {
it('transfers a MessagePort object in the transfer list to worker as event.ports[0]', async () => {
expect.assertions(1)

const worker = new MyWorker()
Expand All @@ -238,6 +238,20 @@ cloneTypes.forEach((clone) => {
worker.postMessage('hello', [channel.port2])
await expect(promise).resolves.toBe('hello world via port')
})

// Skipped for 'ponyfill' because it does not support transferring MessagePort objects.
it.skipIf(clone === 'ponyfill')('transfers a MessagePort object in the data argument to worker and the passed port works', async () => {
expect.assertions(1)

const worker = new MyWorker()
const channel = new MessageChannel()
const promise = new Promise<string>((resolve, reject) => {
channel.port1.onmessage = e => resolve(e.data as string)
channel.port1.onmessageerror = reject
})
worker.postMessage({ port: channel.port2 }, [channel.port2])
await expect(promise).resolves.toBe('Reply via port in data')
})
})
})

Expand Down
Loading