validation.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. // Validate the IPC channel is connected before receiving/sending messages
  2. export const validateIpcMethod = ({methodName, isSubprocess, ipc, isConnected}) => {
  3. validateIpcOption(methodName, isSubprocess, ipc);
  4. validateConnection(methodName, isSubprocess, isConnected);
  5. };
  6. // Better error message when forgetting to set `ipc: true` and using the IPC methods
  7. const validateIpcOption = (methodName, isSubprocess, ipc) => {
  8. if (!ipc) {
  9. throw new Error(`${getMethodName(methodName, isSubprocess)} can only be used if the \`ipc\` option is \`true\`.`);
  10. }
  11. };
  12. // Better error message when one process does not send/receive messages once the other process has disconnected.
  13. // This also makes it clear that any buffered messages are lost once either process has disconnected.
  14. // Also when aborting `cancelSignal` after disconnecting the IPC.
  15. export const validateConnection = (methodName, isSubprocess, isConnected) => {
  16. if (!isConnected) {
  17. throw new Error(`${getMethodName(methodName, isSubprocess)} cannot be used: the ${getOtherProcessName(isSubprocess)} has already exited or disconnected.`);
  18. }
  19. };
  20. // When `getOneMessage()` could not complete due to an early disconnection
  21. export const throwOnEarlyDisconnect = isSubprocess => {
  22. throw new Error(`${getMethodName('getOneMessage', isSubprocess)} could not complete: the ${getOtherProcessName(isSubprocess)} exited or disconnected.`);
  23. };
  24. // When both processes use `sendMessage()` with `strict` at the same time
  25. export const throwOnStrictDeadlockError = isSubprocess => {
  26. throw new Error(`${getMethodName('sendMessage', isSubprocess)} failed: the ${getOtherProcessName(isSubprocess)} is sending a message too, instead of listening to incoming messages.
  27. This can be fixed by both sending a message and listening to incoming messages at the same time:
  28. const [receivedMessage] = await Promise.all([
  29. ${getMethodName('getOneMessage', isSubprocess)},
  30. ${getMethodName('sendMessage', isSubprocess, 'message, {strict: true}')},
  31. ]);`);
  32. };
  33. // When the other process used `strict` but the current process had I/O error calling `sendMessage()` for the response
  34. export const getStrictResponseError = (error, isSubprocess) => new Error(`${getMethodName('sendMessage', isSubprocess)} failed when sending an acknowledgment response to the ${getOtherProcessName(isSubprocess)}.`, {cause: error});
  35. // When using `strict` but the other process was not listening for messages
  36. export const throwOnMissingStrict = isSubprocess => {
  37. throw new Error(`${getMethodName('sendMessage', isSubprocess)} failed: the ${getOtherProcessName(isSubprocess)} is not listening to incoming messages.`);
  38. };
  39. // When using `strict` but the other process disconnected before receiving the message
  40. export const throwOnStrictDisconnect = isSubprocess => {
  41. throw new Error(`${getMethodName('sendMessage', isSubprocess)} failed: the ${getOtherProcessName(isSubprocess)} exited without listening to incoming messages.`);
  42. };
  43. // When the current process disconnects while the subprocess is listening to `cancelSignal`
  44. export const getAbortDisconnectError = () => new Error(`\`cancelSignal\` aborted: the ${getOtherProcessName(true)} disconnected.`);
  45. // When the subprocess uses `cancelSignal` but not the current process
  46. export const throwOnMissingParent = () => {
  47. throw new Error('`getCancelSignal()` cannot be used without setting the `cancelSignal` subprocess option.');
  48. };
  49. // EPIPE can happen when sending a message to a subprocess that is closing but has not disconnected yet
  50. export const handleEpipeError = ({error, methodName, isSubprocess}) => {
  51. if (error.code === 'EPIPE') {
  52. throw new Error(`${getMethodName(methodName, isSubprocess)} cannot be used: the ${getOtherProcessName(isSubprocess)} is disconnecting.`, {cause: error});
  53. }
  54. };
  55. // Better error message when sending messages which cannot be serialized.
  56. // Works with both `serialization: 'advanced'` and `serialization: 'json'`.
  57. export const handleSerializationError = ({error, methodName, isSubprocess, message}) => {
  58. if (isSerializationError(error)) {
  59. throw new Error(`${getMethodName(methodName, isSubprocess)}'s argument type is invalid: the message cannot be serialized: ${String(message)}.`, {cause: error});
  60. }
  61. };
  62. const isSerializationError = ({code, message}) => SERIALIZATION_ERROR_CODES.has(code)
  63. || SERIALIZATION_ERROR_MESSAGES.some(serializationErrorMessage => message.includes(serializationErrorMessage));
  64. // `error.code` set by Node.js when it failed to serialize the message
  65. const SERIALIZATION_ERROR_CODES = new Set([
  66. // Message is `undefined`
  67. 'ERR_MISSING_ARGS',
  68. // Message is a function, a bigint, a symbol
  69. 'ERR_INVALID_ARG_TYPE',
  70. ]);
  71. // `error.message` set by Node.js when it failed to serialize the message
  72. const SERIALIZATION_ERROR_MESSAGES = [
  73. // Message is a promise or a proxy, with `serialization: 'advanced'`
  74. 'could not be cloned',
  75. // Message has cycles, with `serialization: 'json'`
  76. 'circular structure',
  77. // Message has cycles inside toJSON(), with `serialization: 'json'`
  78. 'call stack size exceeded',
  79. ];
  80. const getMethodName = (methodName, isSubprocess, parameters = '') => methodName === 'cancelSignal'
  81. ? '`cancelSignal`\'s `controller.abort()`'
  82. : `${getNamespaceName(isSubprocess)}${methodName}(${parameters})`;
  83. const getNamespaceName = isSubprocess => isSubprocess ? '' : 'subprocess.';
  84. const getOtherProcessName = isSubprocess => isSubprocess ? 'parent process' : 'subprocess';
  85. // When any error arises, we disconnect the IPC.
  86. // Otherwise, it is likely that one of the processes will stop sending/receiving messages.
  87. // This would leave the other process hanging.
  88. export const disconnect = anyProcess => {
  89. if (anyProcess.connected) {
  90. anyProcess.disconnect();
  91. }
  92. };