fix(jest-worker): hangs caused by failed assertions with circular values (#15191)

This commit is contained in:
Dmitry Makhnev
2024-07-23 09:38:01 +02:00
committed by GitHub
parent 78011a667f
commit c072e13726
17 changed files with 881 additions and 122 deletions
+2
View File
@@ -82,6 +82,8 @@
- [**BREAKING**] `--testPathPattern` is now `--testPathPatterns`
- [**BREAKING**] Specifying `testPathPatterns` when programmatically calling `watch` must be specified as `new TestPathPatterns(patterns)`, where `TestPathPatterns` can be imported from `@jest/pattern`
- `[jest-reporters, jest-runner]` Unhandled errors without stack get correctly logged to console ([#14619](https://github.com/jestjs/jest/pull/14619))
- `[jest-worker]` Properly handle a circular reference error when worker tries to send an assertion fails where either the expected or actual value is circular ([#15191](https://github.com/jestjs/jest/pull/15191))
- `[jest-worker]` Properly handle a BigInt when worker tries to send an assertion fails where either the expected or actual value is BigInt ([#15191](https://github.com/jestjs/jest/pull/15191))
### Performance
@@ -1,53 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`handles circular inequality properly 1`] = `
"FAIL __tests__/test-1.js
● test
expect(received).toEqual(expected) // deep equality
- Expected - 1
+ Received + 3
- Object {}
+ Object {
+ "ref": [Circular],
+ }
3 | foo.ref = foo;
4 |
> 5 | expect(foo).toEqual({});
| ^
6 | });
at Object.toEqual (__tests__/test-1.js:5:15)
FAIL __tests__/test-2.js
● test
expect(received).toEqual(expected) // deep equality
- Expected - 1
+ Received + 3
- Object {}
+ Object {
+ "ref": [Circular],
+ }
3 | foo.ref = foo;
4 |
> 5 | expect(foo).toEqual({});
| ^
6 | });
at Object.toEqual (__tests__/test-2.js:5:15)"
`;
exports[`handles circular inequality properly 2`] = `
"Test Suites: 2 failed, 2 total
Tests: 2 failed, 2 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites."
`;
@@ -0,0 +1,533 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`processChild handles \`BigInt\` 1`] = `
"FAIL __tests__/test-1.js
● test
expect(received).toBe(expected) // Object.is equality
Expected: 73n
Received: 42n
1 | it('test', () => {
> 2 | expect(BigInt(42)).toBe(BigInt(73));
| ^
3 | });
at Object.toBe (__tests__/test-1.js:2:22)
FAIL __tests__/test-2.js
● test
expect(received).toBe(expected) // Object.is equality
Expected: 73n
Received: 42n
1 | it('test', () => {
> 2 | expect(BigInt(42)).toBe(BigInt(73));
| ^
3 | });
at Object.toBe (__tests__/test-2.js:2:22)"
`;
exports[`processChild handles \`BigInt\` 2`] = `
"Test Suites: 2 failed, 2 total
Tests: 2 failed, 2 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites."
`;
exports[`processChild handles \`Map\` 1`] = `
"FAIL __tests__/test-1.js
● test
expect(received).toEqual(expected) // deep equality
- Expected - 1
+ Received + 1
Map {
- 1 => "3",
+ 1 => "2",
}
1 | it('test', () => {
> 2 | expect(new Map([[1, "2"]])).toEqual(new Map([[1, "3"]]));
| ^
3 | });
at Object.toEqual (__tests__/test-1.js:2:31)
FAIL __tests__/test-2.js
● test
expect(received).toEqual(expected) // deep equality
- Expected - 1
+ Received + 1
Map {
- 1 => "3",
+ 1 => "2",
}
1 | it('test', () => {
> 2 | expect(new Map([[1, "2"]])).toEqual(new Map([[1, "3"]]));
| ^
3 | });
at Object.toEqual (__tests__/test-2.js:2:31)"
`;
exports[`processChild handles \`Map\` 2`] = `
"Test Suites: 2 failed, 2 total
Tests: 2 failed, 2 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites."
`;
exports[`processChild handles \`Symbol\` 1`] = `
"FAIL __tests__/test-1.js
● test
expect(received).toEqual(expected) // deep equality
Expected: Symbol(b)
Received: Symbol(a)
1 | it('test', () => {
> 2 | expect(Symbol('a')).toEqual(Symbol('b'));
| ^
3 | });
at Object.toEqual (__tests__/test-1.js:2:23)
FAIL __tests__/test-2.js
● test
expect(received).toEqual(expected) // deep equality
Expected: Symbol(b)
Received: Symbol(a)
1 | it('test', () => {
> 2 | expect(Symbol('a')).toEqual(Symbol('b'));
| ^
3 | });
at Object.toEqual (__tests__/test-2.js:2:23)"
`;
exports[`processChild handles \`Symbol\` 2`] = `
"Test Suites: 2 failed, 2 total
Tests: 2 failed, 2 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites."
`;
exports[`processChild handles circular inequality properly 1`] = `
"FAIL __tests__/test-1.js
● test
expect(received).toEqual(expected) // deep equality
- Expected - 1
+ Received + 3
- Object {}
+ Object {
+ "ref": [Circular],
+ }
3 | foo.ref = foo;
4 |
> 5 | expect(foo).toEqual({});
| ^
6 | });
at Object.toEqual (__tests__/test-1.js:5:15)
FAIL __tests__/test-2.js
● test
expect(received).toEqual(expected) // deep equality
- Expected - 1
+ Received + 3
- Object {}
+ Object {
+ "ref": [Circular],
+ }
3 | foo.ref = foo;
4 |
> 5 | expect(foo).toEqual({});
| ^
6 | });
at Object.toEqual (__tests__/test-2.js:5:15)"
`;
exports[`processChild handles circular inequality properly 2`] = `
"Test Suites: 2 failed, 2 total
Tests: 2 failed, 2 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites."
`;
exports[`processChild handles functions 1`] = `
"FAIL __tests__/test-1.js
● test
expect(received).toEqual(expected) // deep equality
Expected: [Function fn2]
Received: [Function fn1]
2 | const fn1 = () => {};
3 | const fn2 = () => {};
> 4 | expect(fn1).toEqual(fn2);
| ^
5 | });
at Object.toEqual (__tests__/test-1.js:4:15)
FAIL __tests__/test-2.js
● test
expect(received).toEqual(expected) // deep equality
Expected: [Function fn2]
Received: [Function fn1]
2 | const fn1 = () => {};
3 | const fn2 = () => {};
> 4 | expect(fn1).toEqual(fn2);
| ^
5 | });
at Object.toEqual (__tests__/test-2.js:4:15)"
`;
exports[`processChild handles functions 2`] = `
"Test Suites: 2 failed, 2 total
Tests: 2 failed, 2 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites."
`;
exports[`processChild handles mixed structure 1`] = `
"FAIL __tests__/test-1.js
● test
expect(received).toEqual(expected) // deep equality
Expected: false
Received: {"bigInt": 42n, "fn": [Function anonymous], "map": Map {1 => "2"}, "ref": [Circular], "symbol": Symbol(asd), Symbol(qwe): Symbol(zxc)}
11 | method() {}
12 | }
> 13 | expect(new Class()).toEqual(false);
| ^
14 | });
at Object.toEqual (__tests__/test-1.js:13:23)
FAIL __tests__/test-2.js
● test
expect(received).toEqual(expected) // deep equality
Expected: false
Received: {"bigInt": 42n, "fn": [Function anonymous], "map": Map {1 => "2"}, "ref": [Circular], "symbol": Symbol(asd), Symbol(qwe): Symbol(zxc)}
11 | method() {}
12 | }
> 13 | expect(new Class()).toEqual(false);
| ^
14 | });
at Object.toEqual (__tests__/test-2.js:13:23)"
`;
exports[`processChild handles mixed structure 2`] = `
"Test Suites: 2 failed, 2 total
Tests: 2 failed, 2 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites."
`;
exports[`workerThreads handles \`BigInt\` 1`] = `
"FAIL __tests__/test-1.js
● test
expect(received).toBe(expected) // Object.is equality
Expected: 73n
Received: 42n
1 | it('test', () => {
> 2 | expect(BigInt(42)).toBe(BigInt(73));
| ^
3 | });
at Object.toBe (__tests__/test-1.js:2:22)
FAIL __tests__/test-2.js
● test
expect(received).toBe(expected) // Object.is equality
Expected: 73n
Received: 42n
1 | it('test', () => {
> 2 | expect(BigInt(42)).toBe(BigInt(73));
| ^
3 | });
at Object.toBe (__tests__/test-2.js:2:22)"
`;
exports[`workerThreads handles \`BigInt\` 2`] = `
"Test Suites: 2 failed, 2 total
Tests: 2 failed, 2 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites."
`;
exports[`workerThreads handles \`Map\` 1`] = `
"FAIL __tests__/test-1.js
● test
expect(received).toEqual(expected) // deep equality
- Expected - 1
+ Received + 1
Map {
- 1 => "3",
+ 1 => "2",
}
1 | it('test', () => {
> 2 | expect(new Map([[1, "2"]])).toEqual(new Map([[1, "3"]]));
| ^
3 | });
at Object.toEqual (__tests__/test-1.js:2:31)
FAIL __tests__/test-2.js
● test
expect(received).toEqual(expected) // deep equality
- Expected - 1
+ Received + 1
Map {
- 1 => "3",
+ 1 => "2",
}
1 | it('test', () => {
> 2 | expect(new Map([[1, "2"]])).toEqual(new Map([[1, "3"]]));
| ^
3 | });
at Object.toEqual (__tests__/test-2.js:2:31)"
`;
exports[`workerThreads handles \`Map\` 2`] = `
"Test Suites: 2 failed, 2 total
Tests: 2 failed, 2 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites."
`;
exports[`workerThreads handles \`Symbol\` 1`] = `
"FAIL __tests__/test-1.js
● test
expect(received).toEqual(expected) // deep equality
Expected: Symbol(b)
Received: Symbol(a)
1 | it('test', () => {
> 2 | expect(Symbol('a')).toEqual(Symbol('b'));
| ^
3 | });
at Object.toEqual (__tests__/test-1.js:2:23)
FAIL __tests__/test-2.js
● test
expect(received).toEqual(expected) // deep equality
Expected: Symbol(b)
Received: Symbol(a)
1 | it('test', () => {
> 2 | expect(Symbol('a')).toEqual(Symbol('b'));
| ^
3 | });
at Object.toEqual (__tests__/test-2.js:2:23)"
`;
exports[`workerThreads handles \`Symbol\` 2`] = `
"Test Suites: 2 failed, 2 total
Tests: 2 failed, 2 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites."
`;
exports[`workerThreads handles circular inequality properly 1`] = `
"FAIL __tests__/test-1.js
● test
expect(received).toEqual(expected) // deep equality
- Expected - 1
+ Received + 3
- Object {}
+ Object {
+ "ref": [Circular],
+ }
3 | foo.ref = foo;
4 |
> 5 | expect(foo).toEqual({});
| ^
6 | });
at Object.toEqual (__tests__/test-1.js:5:15)
FAIL __tests__/test-2.js
● test
expect(received).toEqual(expected) // deep equality
- Expected - 1
+ Received + 3
- Object {}
+ Object {
+ "ref": [Circular],
+ }
3 | foo.ref = foo;
4 |
> 5 | expect(foo).toEqual({});
| ^
6 | });
at Object.toEqual (__tests__/test-2.js:5:15)"
`;
exports[`workerThreads handles circular inequality properly 2`] = `
"Test Suites: 2 failed, 2 total
Tests: 2 failed, 2 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites."
`;
exports[`workerThreads handles functions 1`] = `
"FAIL __tests__/test-1.js
● test
expect(received).toEqual(expected) // deep equality
Expected: [Function fn2]
Received: [Function fn1]
2 | const fn1 = () => {};
3 | const fn2 = () => {};
> 4 | expect(fn1).toEqual(fn2);
| ^
5 | });
at Object.toEqual (__tests__/test-1.js:4:15)
FAIL __tests__/test-2.js
● test
expect(received).toEqual(expected) // deep equality
Expected: [Function fn2]
Received: [Function fn1]
2 | const fn1 = () => {};
3 | const fn2 = () => {};
> 4 | expect(fn1).toEqual(fn2);
| ^
5 | });
at Object.toEqual (__tests__/test-2.js:4:15)"
`;
exports[`workerThreads handles functions 2`] = `
"Test Suites: 2 failed, 2 total
Tests: 2 failed, 2 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites."
`;
exports[`workerThreads handles mixed structure 1`] = `
"FAIL __tests__/test-1.js
● test
expect(received).toEqual(expected) // deep equality
Expected: false
Received: {"bigInt": 42n, "fn": [Function anonymous], "map": Map {1 => "2"}, "ref": [Circular], "symbol": Symbol(asd), Symbol(qwe): Symbol(zxc)}
11 | method() {}
12 | }
> 13 | expect(new Class()).toEqual(false);
| ^
14 | });
at Object.toEqual (__tests__/test-1.js:13:23)
FAIL __tests__/test-2.js
● test
expect(received).toEqual(expected) // deep equality
Expected: false
Received: {"bigInt": 42n, "fn": [Function anonymous], "map": Map {1 => "2"}, "ref": [Circular], "symbol": Symbol(asd), Symbol(qwe): Symbol(zxc)}
11 | method() {}
12 | }
> 13 | expect(new Class()).toEqual(false);
| ^
14 | });
at Object.toEqual (__tests__/test-2.js:13:23)"
`;
exports[`workerThreads handles mixed structure 2`] = `
"Test Suites: 2 failed, 2 total
Tests: 2 failed, 2 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites."
`;
-57
View File
@@ -1,57 +0,0 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {tmpdir} from 'os';
import * as path from 'path';
import {
cleanup,
createEmptyPackage,
extractSortedSummary,
writeFiles,
} from '../Utils';
import {runContinuous} from '../runJest';
const tempDir = path.resolve(tmpdir(), 'circular-inequality-test');
beforeEach(() => {
createEmptyPackage(tempDir);
});
afterEach(() => {
cleanup(tempDir);
});
test.skip('handles circular inequality properly', async () => {
const testFileContent = `
it('test', () => {
const foo = {};
foo.ref = foo;
expect(foo).toEqual({});
});
`;
writeFiles(tempDir, {
'__tests__/test-1.js': testFileContent,
'__tests__/test-2.js': testFileContent,
});
const {end, waitUntil} = runContinuous(
tempDir,
['--no-watchman', '--watch-all'],
// timeout in case the `waitUntil` below doesn't fire
{stripAnsi: true, timeout: 5000},
);
await waitUntil(({stderr}) => stderr.includes('Ran all test suites.'));
const {stderr} = await end();
const {summary, rest} = extractSortedSummary(stderr);
expect(rest).toMatchSnapshot();
expect(summary).toMatchSnapshot();
});
@@ -0,0 +1,148 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {tmpdir} from 'os';
import * as path from 'path';
import {
cleanup,
createEmptyPackage,
extractSortedSummary,
writeFiles,
} from '../Utils';
import {runContinuous} from '../runJest';
const tempDir = path.resolve(tmpdir(), 'bigint-inequality-test');
const testIn2Workers = async (
testFileContent: string,
extraOptions: Array<string> = [],
) => {
writeFiles(tempDir, {
'__tests__/test-1.js': testFileContent,
'__tests__/test-2.js': testFileContent,
});
const {end, waitUntil} = runContinuous(
tempDir,
['--no-watchman', '--watch-all', ...extraOptions],
// timeout in case the `waitUntil` below doesn't fire
{stripAnsi: true, timeout: 5000},
);
await waitUntil(({stderr}) => stderr.includes('Ran all test suites.'));
const {stderr} = await end();
return extractSortedSummary(stderr);
};
beforeEach(() => {
createEmptyPackage(tempDir);
});
afterEach(() => {
cleanup(tempDir);
});
describe.each([
{name: 'processChild'},
{extraOptions: ['--workerThreads'], name: 'workerThreads'},
])('$name', ({extraOptions}) => {
test('handles circular inequality properly', async () => {
const {summary, rest} = await testIn2Workers(
`
it('test', () => {
const foo = {};
foo.ref = foo;
expect(foo).toEqual({});
});
`,
extraOptions,
);
expect(rest).toMatchSnapshot();
expect(summary).toMatchSnapshot();
});
test('handles `Map`', async () => {
const {summary, rest} = await testIn2Workers(
`
it('test', () => {
expect(new Map([[1, "2"]])).toEqual(new Map([[1, "3"]]));
});
`,
extraOptions,
);
expect(rest).toMatchSnapshot();
expect(summary).toMatchSnapshot();
});
test('handles `BigInt`', async () => {
const {summary, rest} = await testIn2Workers(
`
it('test', () => {
expect(BigInt(42)).toBe(BigInt(73));
});
`,
extraOptions,
);
expect(rest).toMatchSnapshot();
expect(summary).toMatchSnapshot();
});
test('handles `Symbol`', async () => {
const {summary, rest} = await testIn2Workers(
`
it('test', () => {
expect(Symbol('a')).toEqual(Symbol('b'));
});
`,
extraOptions,
);
expect(rest).toMatchSnapshot();
expect(summary).toMatchSnapshot();
});
test('handles functions', async () => {
const {summary, rest} = await testIn2Workers(
`
it('test', () => {
const fn1 = () => {};
const fn2 = () => {};
expect(fn1).toEqual(fn2);
});
`,
extraOptions,
);
expect(rest).toMatchSnapshot();
expect(summary).toMatchSnapshot();
});
test('handles mixed structure', async () => {
const {summary, rest} = await testIn2Workers(
`
it('test', () => {
class Class {
constructor() {
this.ref = this;
this.bigInt = BigInt(42);
this.map = new Map([[1, "2"]]);
this.symbol = Symbol('asd');
this[Symbol('qwe')] = Symbol('zxc');
this.fn = () => {};
}
method() {}
}
expect(new Class()).toEqual(false);
});
`,
extraOptions,
);
expect(rest).toMatchSnapshot();
expect(summary).toMatchSnapshot();
});
});
+5
View File
@@ -29,6 +29,11 @@ export type AssertionResult = {
* was used.
*/
failing?: boolean;
/**
* The raw values of the `function` or `symbol` types will be lost in some cases
* because it's not possible to serialize them correctly between workers.
* However, information about them will be available in the `failureMessages`.
*/
failureDetails: Array<unknown>;
failureMessages: Array<string>;
fullName: string;
+2
View File
@@ -20,6 +20,7 @@
},
"dependencies": {
"@types/node": "*",
"@ungap/structured-clone": "^1.2.0",
"jest-util": "workspace:*",
"merge-stream": "^2.0.0",
"supports-color": "^8.0.0"
@@ -28,6 +29,7 @@
"@babel/core": "^7.11.6",
"@types/merge-stream": "^1.1.2",
"@types/supports-color": "^8.1.0",
"@types/ungap__structured-clone": "^1.2.0",
"get-stream": "^6.0.0",
"jest-leak-detector": "workspace:*",
"worker-farm": "^1.6.0"
@@ -27,6 +27,7 @@ import {
WorkerStates,
} from '../types';
import WorkerAbstract from './WorkerAbstract';
import {unpackMessage} from './safeMessageTransferring';
const SIGNAL_BASE_EXIT_CODE = 128;
const SIGKILL_EXIT_CODE = SIGNAL_BASE_EXIT_CODE + 9;
@@ -263,7 +264,7 @@ export default class ChildProcessWorker
switch (response[0]) {
case PARENT_MESSAGE_OK:
this._onProcessEnd(null, response[1]);
this._onProcessEnd(null, unpackMessage(response[1]));
break;
case PARENT_MESSAGE_CLIENT_ERROR:
@@ -297,7 +298,7 @@ export default class ChildProcessWorker
break;
case PARENT_MESSAGE_CUSTOM:
this._onCustomMessage(response[1]);
this._onCustomMessage(unpackMessage(response[1]));
break;
case PARENT_MESSAGE_MEM_USAGE:
@@ -26,6 +26,7 @@ import {
WorkerStates,
} from '../types';
import WorkerAbstract from './WorkerAbstract';
import {unpackMessage} from './safeMessageTransferring';
export default class ExperimentalWorker
extends WorkerAbstract
@@ -177,7 +178,7 @@ export default class ExperimentalWorker
switch (response[0]) {
case PARENT_MESSAGE_OK:
this._onProcessEnd(null, response[1]);
this._onProcessEnd(null, unpackMessage(response[1]));
break;
case PARENT_MESSAGE_CLIENT_ERROR:
@@ -213,7 +214,7 @@ export default class ExperimentalWorker
break;
case PARENT_MESSAGE_CUSTOM:
this._onCustomMessage(response[1]);
this._onCustomMessage(unpackMessage(response[1]));
break;
case PARENT_MESSAGE_MEM_USAGE:
@@ -20,7 +20,13 @@ import ThreadsWorker from '../NodeThreadsWorker';
jest.setTimeout(10_000);
const root = join('../../');
const filesToBuild = ['workers/processChild', 'workers/threadChild', 'types'];
const filesToBuild = [
'workers/processChild',
'workers/threadChild',
'workers/safeMessageTransferring',
'workers/isDataCloneError',
'types',
];
const writeDestination = join(__dirname, '__temp__');
const processChildWorkerPath = join(
writeDestination,
@@ -41,6 +41,12 @@ beforeEach(() => {
mockCount++;
return {
fooCircularResult() {
const circular = {self: undefined as unknown};
circular.self = circular;
return {error: circular};
},
fooPromiseThrows() {
return new Promise((_resolve, reject) => {
setTimeout(() => reject(mockError), 5);
@@ -338,6 +344,32 @@ it('returns results when it gets resolved if function is asynchronous', async ()
expect(spyProcessSend).toHaveBeenCalledTimes(2);
});
it('returns results with circular references', () => {
process.emit(
'message',
[
CHILD_MESSAGE_INITIALIZE,
true, // Not really used here, but for type purity.
'./my-fancy-worker',
],
null,
);
process.emit(
'message',
[
CHILD_MESSAGE_CALL,
true, // Not really used here, but for type purity.
'fooCircularResult',
[],
],
null,
);
const processCallError = spyProcessSend.mock.calls[0][0][1].error;
expect(processCallError.self).toBe(processCallError.self.self);
});
it('calls the main module if the method call is "default"', () => {
process.emit(
'message',
@@ -0,0 +1,27 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
// https://webidl.spec.whatwg.org/#datacloneerror
const DATA_CLONE_ERROR_CODE = 25;
/**
* Unfortunately, [`util.types.isNativeError(value)`](https://nodejs.org/api/util.html#utiltypesisnativeerrorvalue)
* return `false` for `DataCloneError` error.
* For this reason, try to detect it in this way
*/
export function isDataCloneError(error: unknown): error is DOMException {
return (
error != null &&
typeof error === 'object' &&
'name' in error &&
error.name === 'DataCloneError' &&
'message' in error &&
typeof error.message === 'string' &&
'code' in error &&
error.code === DATA_CLONE_ERROR_CODE
);
}
@@ -5,17 +5,44 @@
* LICENSE file in the root directory of this source tree.
*/
import {types} from 'node:util';
import {isMainThread, parentPort} from 'worker_threads';
import {PARENT_MESSAGE_CUSTOM} from '../types';
import {isDataCloneError} from './isDataCloneError';
import {packMessage} from './safeMessageTransferring';
export default function messageParent(
message: unknown,
parentProcess = process,
): void {
if (!isMainThread && parentPort != null) {
parentPort.postMessage([PARENT_MESSAGE_CUSTOM, message]);
try {
parentPort.postMessage([PARENT_MESSAGE_CUSTOM, message]);
} catch (error) {
// Try to handle https://html.spec.whatwg.org/multipage/structured-data.html#structuredserializeinternal
// for `symbols` and `functions`
if (isDataCloneError(error)) {
parentPort.postMessage([PARENT_MESSAGE_CUSTOM, packMessage(message)]);
} else {
throw error;
}
}
} else if (typeof parentProcess.send === 'function') {
parentProcess.send([PARENT_MESSAGE_CUSTOM, message]);
try {
parentProcess.send([PARENT_MESSAGE_CUSTOM, message]);
} catch (error) {
if (
types.isNativeError(error) &&
// if .send is a function, it's a serialization issue
!error.message.includes('.send is not a function')
) {
// Apply specific serialization only in error cases
// to avoid affecting performance in regular cases.
parentProcess.send([PARENT_MESSAGE_CUSTOM, packMessage(message)]);
} else {
throw error;
}
}
} else {
throw new TypeError('"messageParent" can only be used inside a worker');
}
@@ -5,6 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
import {types} from 'node:util';
import {isPromise} from 'jest-util';
import {
CHILD_MESSAGE_CALL,
@@ -21,6 +22,7 @@ import {
PARENT_MESSAGE_SETUP_ERROR,
type ParentMessageMemUsage,
} from '../types';
import {packMessage} from './safeMessageTransferring';
type UnknownFunction = (...args: Array<unknown>) => unknown | Promise<unknown>;
@@ -97,7 +99,21 @@ function reportSuccess(result: unknown) {
throw new Error('Child can only be used on a forked process');
}
process.send([PARENT_MESSAGE_OK, result]);
try {
process.send([PARENT_MESSAGE_OK, result]);
} catch (error) {
if (
types.isNativeError(error) &&
// if .send is a function, it's a serialization issue
!error.message.includes('.send is not a function')
) {
// Apply specific serialization only in error cases
// to avoid affecting performance in regular cases.
process.send([PARENT_MESSAGE_OK, packMessage(result)]);
} else {
throw error;
}
}
}
function reportClientError(error: Error) {
@@ -0,0 +1,48 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {
type SerializedRecord,
deserialize,
serialize,
} from '@ungap/structured-clone';
type TransferringContainer = {
__STRUCTURED_CLONE_SERIALIZED__: true;
data: SerializedRecord;
};
export function packMessage(message: unknown): TransferringContainer {
return {
__STRUCTURED_CLONE_SERIALIZED__: true,
/**
* Use the `json: true` option to avoid errors
* caused by `function` or `symbol` types.
* It's not ideal to lose `function` and `symbol` types,
* but reliability is more important.
*/
data: serialize(message, {json: true}),
};
}
function isTransferringContainer(
message: unknown,
): message is TransferringContainer {
return (
message != null &&
typeof message === 'object' &&
'__STRUCTURED_CLONE_SERIALIZED__' in message &&
'data' in message
);
}
export function unpackMessage(message: unknown): unknown {
if (isTransferringContainer(message)) {
return deserialize(message.data);
}
return message;
}
@@ -22,6 +22,8 @@ import {
PARENT_MESSAGE_SETUP_ERROR,
type ParentMessageMemUsage,
} from '../types';
import {isDataCloneError} from './isDataCloneError';
import {packMessage} from './safeMessageTransferring';
type UnknownFunction = (...args: Array<unknown>) => unknown | Promise<unknown>;
@@ -114,11 +116,21 @@ function reportSuccess(result: unknown) {
try {
parentPort!.postMessage([PARENT_MESSAGE_OK, result]);
} catch (error: any) {
// Handling it here to avoid unhandled `DataCloneError` rejection
} catch (error) {
let resolvedError = error;
// Try to handle https://html.spec.whatwg.org/multipage/structured-data.html#structuredserializeinternal
// for `symbols` and `functions`
if (isDataCloneError(error)) {
try {
parentPort!.postMessage([PARENT_MESSAGE_OK, packMessage(result)]);
return;
} catch (secondTryError) {
resolvedError = secondTryError;
}
}
// Handling it here to avoid unhandled rejection
// which is hard to distinguish on the parent side
// (such error doesn't have any message or stack trace)
reportClientError(error);
reportClientError(resolvedError as Error);
}
}
+9
View File
@@ -5902,6 +5902,13 @@ __metadata:
languageName: node
linkType: hard
"@types/ungap__structured-clone@npm:^1.2.0":
version: 1.2.0
resolution: "@types/ungap__structured-clone@npm:1.2.0"
checksum: 5399cfc129ecae2986fbe223fdf8f443c27a628e0d24e1be0fd3d041d643e9a041be54d9b9511fdf36279e7a3cb4c9d72493f30d75fdcde52efa3ac9dc0ebb1e
languageName: node
linkType: hard
"@types/unist@npm:*, @types/unist@npm:^3.0.0":
version: 3.0.2
resolution: "@types/unist@npm:3.0.2"
@@ -13711,6 +13718,8 @@ __metadata:
"@types/merge-stream": ^1.1.2
"@types/node": "*"
"@types/supports-color": ^8.1.0
"@types/ungap__structured-clone": ^1.2.0
"@ungap/structured-clone": ^1.2.0
get-stream: ^6.0.0
jest-leak-detector: "workspace:*"
jest-util: "workspace:*"