@@ -1699,4 +1699,251 @@ describe('network.initNetwork', () => {
16991699 } ) ;
17001700 } ) ;
17011701 } ) ;
1702+
1703+ describe ( 'message send timeout' , ( ) => {
1704+ afterEach ( ( ) => {
1705+ vi . restoreAllMocks ( ) ;
1706+ } ) ;
1707+
1708+ it ( 'times out after 10 seconds when write hangs' , async ( ) => {
1709+ // Ensure isReconnecting returns false so we actually call writeWithTimeout
1710+ mockReconnectionManager . isReconnecting . mockReturnValue ( false ) ;
1711+
1712+ const mockChannel = createMockChannel ( 'peer-1' ) ;
1713+ // Make write hang indefinitely - return a new hanging promise each time
1714+ mockChannel . msgStream . write . mockReset ( ) ;
1715+ mockChannel . msgStream . write . mockImplementation (
1716+ async ( ) =>
1717+ new Promise < never > ( ( ) => {
1718+ // Never resolves - simulates hanging write
1719+ } ) ,
1720+ ) ;
1721+ mockConnectionFactory . dialIdempotent . mockResolvedValue ( mockChannel ) ;
1722+
1723+ let mockSignal : ReturnType < typeof makeAbortSignalMock > | undefined ;
1724+ vi . spyOn ( AbortSignal , 'timeout' ) . mockImplementation ( ( ms : number ) => {
1725+ mockSignal = makeAbortSignalMock ( ms ) ;
1726+ return mockSignal ;
1727+ } ) ;
1728+
1729+ const { sendRemoteMessage } = await initNetwork ( '0x1234' , { } , vi . fn ( ) ) ;
1730+
1731+ const sendPromise = sendRemoteMessage ( 'peer-1' , 'test message' ) ;
1732+
1733+ // Wait for the promise to be set up and event listener registered
1734+ await new Promise < void > ( ( resolve ) => queueMicrotask ( ( ) => resolve ( ) ) ) ;
1735+
1736+ // Verify write was called (proves we're not returning early)
1737+ expect ( mockChannel . msgStream . write ) . toHaveBeenCalled ( ) ;
1738+
1739+ // Manually trigger the abort to simulate timeout
1740+ mockSignal ?. abort ( ) ;
1741+
1742+ // Wait for the abort handler to execute
1743+ await new Promise < void > ( ( resolve ) => queueMicrotask ( ( ) => resolve ( ) ) ) ;
1744+
1745+ // Note: sendRemoteMessage catches the timeout error and returns undefined
1746+ // The timeout error is handled internally and triggers connection loss handling
1747+ expect ( await sendPromise ) . toBeUndefined ( ) ;
1748+
1749+ // Verify that connection loss handling was triggered
1750+ expect ( mockReconnectionManager . startReconnection ) . toHaveBeenCalled ( ) ;
1751+ } ) ;
1752+
1753+ it ( 'does not timeout if write completes before timeout' , async ( ) => {
1754+ const mockChannel = createMockChannel ( 'peer-1' ) ;
1755+ mockChannel . msgStream . write . mockResolvedValue ( undefined ) ;
1756+ mockConnectionFactory . dialIdempotent . mockResolvedValue ( mockChannel ) ;
1757+
1758+ let mockSignal : ReturnType < typeof makeAbortSignalMock > | undefined ;
1759+ vi . spyOn ( AbortSignal , 'timeout' ) . mockImplementation ( ( ms : number ) => {
1760+ mockSignal = makeAbortSignalMock ( ms ) ;
1761+ return mockSignal ;
1762+ } ) ;
1763+
1764+ const { sendRemoteMessage } = await initNetwork ( '0x1234' , { } , vi . fn ( ) ) ;
1765+
1766+ const sendPromise = sendRemoteMessage ( 'peer-1' , 'test message' ) ;
1767+
1768+ // Write resolves immediately, so promise should resolve
1769+ expect ( await sendPromise ) . toBeUndefined ( ) ;
1770+
1771+ // Verify timeout signal was not aborted
1772+ expect ( mockSignal ?. aborted ) . toBe ( false ) ;
1773+ } ) ;
1774+
1775+ it ( 'handles timeout errors and triggers connection loss handling' , async ( ) => {
1776+ // Ensure isReconnecting returns false so we actually call writeWithTimeout
1777+ mockReconnectionManager . isReconnecting . mockReturnValue ( false ) ;
1778+
1779+ const mockChannel = createMockChannel ( 'peer-1' ) ;
1780+ // Make write hang indefinitely - return a new hanging promise each time
1781+ mockChannel . msgStream . write . mockReset ( ) ;
1782+ mockChannel . msgStream . write . mockImplementation (
1783+ async ( ) =>
1784+ new Promise < never > ( ( ) => {
1785+ // Never resolves - simulates hanging write
1786+ } ) ,
1787+ ) ;
1788+ mockConnectionFactory . dialIdempotent . mockResolvedValue ( mockChannel ) ;
1789+
1790+ let mockSignal : ReturnType < typeof makeAbortSignalMock > | undefined ;
1791+ vi . spyOn ( AbortSignal , 'timeout' ) . mockImplementation ( ( ms : number ) => {
1792+ mockSignal = makeAbortSignalMock ( ms ) ;
1793+ return mockSignal ;
1794+ } ) ;
1795+
1796+ const { sendRemoteMessage } = await initNetwork ( '0x1234' , { } , vi . fn ( ) ) ;
1797+
1798+ const sendPromise = sendRemoteMessage ( 'peer-1' , 'test message' ) ;
1799+
1800+ // Wait for the promise to be set up and event listener registered
1801+ await new Promise < void > ( ( resolve ) => queueMicrotask ( ( ) => resolve ( ) ) ) ;
1802+
1803+ // Manually trigger the abort to simulate timeout
1804+ mockSignal ?. abort ( ) ;
1805+
1806+ // Wait for the abort handler to execute
1807+ await new Promise < void > ( ( resolve ) => queueMicrotask ( ( ) => resolve ( ) ) ) ;
1808+
1809+ // Note: sendRemoteMessage catches the timeout error and returns undefined
1810+ // The timeout error is handled internally and triggers connection loss handling
1811+ expect ( await sendPromise ) . toBeUndefined ( ) ;
1812+
1813+ // Verify that connection loss handling was triggered
1814+ expect ( mockReconnectionManager . startReconnection ) . toHaveBeenCalled ( ) ;
1815+ } ) ;
1816+
1817+ it ( 'propagates write errors that occur before timeout' , async ( ) => {
1818+ // Ensure isReconnecting returns false so we actually call writeWithTimeout
1819+ mockReconnectionManager . isReconnecting . mockReturnValue ( false ) ;
1820+
1821+ const mockChannel = createMockChannel ( 'peer-1' ) ;
1822+ const writeError = new Error ( 'Write failed' ) ;
1823+ mockChannel . msgStream . write . mockRejectedValue ( writeError ) ;
1824+ mockConnectionFactory . dialIdempotent . mockResolvedValue ( mockChannel ) ;
1825+
1826+ const { sendRemoteMessage } = await initNetwork ( '0x1234' , { } , vi . fn ( ) ) ;
1827+
1828+ const sendPromise = sendRemoteMessage ( 'peer-1' , 'test message' ) ;
1829+
1830+ // Write error occurs immediately
1831+ // Note: sendRemoteMessage catches write errors and returns undefined
1832+ // The error is handled internally and triggers connection loss handling
1833+ expect ( await sendPromise ) . toBeUndefined ( ) ;
1834+
1835+ // Verify that connection loss handling was triggered
1836+ expect ( mockReconnectionManager . startReconnection ) . toHaveBeenCalled ( ) ;
1837+ } ) ;
1838+
1839+ it ( 'writeWithTimeout uses AbortSignal.timeout with 10 second default' , async ( ) => {
1840+ const mockChannel = createMockChannel ( 'peer-1' ) ;
1841+ // Make write resolve immediately to avoid timeout
1842+ mockChannel . msgStream . write . mockResolvedValue ( undefined ) ;
1843+ mockConnectionFactory . dialIdempotent . mockResolvedValue ( mockChannel ) ;
1844+
1845+ let mockSignal : ReturnType < typeof makeAbortSignalMock > | undefined ;
1846+ vi . spyOn ( AbortSignal , 'timeout' ) . mockImplementation ( ( ms : number ) => {
1847+ mockSignal = makeAbortSignalMock ( ms ) ;
1848+ return mockSignal ;
1849+ } ) ;
1850+
1851+ const { sendRemoteMessage } = await initNetwork ( '0x1234' , { } , vi . fn ( ) ) ;
1852+
1853+ await sendRemoteMessage ( 'peer-1' , 'test message' ) ;
1854+
1855+ // Verify AbortSignal.timeout was called with 10 seconds (default)
1856+ expect ( AbortSignal . timeout ) . toHaveBeenCalledWith ( 10_000 ) ;
1857+ expect ( mockSignal ?. timeoutMs ) . toBe ( 10_000 ) ;
1858+ } ) ;
1859+
1860+ it ( 'error message includes correct timeout duration' , async ( ) => {
1861+ // Ensure isReconnecting returns false so we actually call writeWithTimeout
1862+ mockReconnectionManager . isReconnecting . mockReturnValue ( false ) ;
1863+
1864+ const mockChannel = createMockChannel ( 'peer-1' ) ;
1865+ // Make write hang indefinitely - return a new hanging promise each time
1866+ mockChannel . msgStream . write . mockReset ( ) ;
1867+ mockChannel . msgStream . write . mockImplementation (
1868+ async ( ) =>
1869+ new Promise < never > ( ( ) => {
1870+ // Never resolves - simulates hanging write
1871+ } ) ,
1872+ ) ;
1873+ mockConnectionFactory . dialIdempotent . mockResolvedValue ( mockChannel ) ;
1874+
1875+ let mockSignal : ReturnType < typeof makeAbortSignalMock > | undefined ;
1876+ vi . spyOn ( AbortSignal , 'timeout' ) . mockImplementation ( ( ms : number ) => {
1877+ mockSignal = makeAbortSignalMock ( ms ) ;
1878+ return mockSignal ;
1879+ } ) ;
1880+
1881+ const { sendRemoteMessage } = await initNetwork ( '0x1234' , { } , vi . fn ( ) ) ;
1882+
1883+ const sendPromise = sendRemoteMessage ( 'peer-1' , 'test message' ) ;
1884+
1885+ // Wait for the promise to be set up and event listener registered
1886+ await new Promise < void > ( ( resolve ) => queueMicrotask ( ( ) => resolve ( ) ) ) ;
1887+
1888+ // Manually trigger the abort to simulate timeout
1889+ mockSignal ?. abort ( ) ;
1890+
1891+ // Wait for the abort handler to execute
1892+ await new Promise < void > ( ( resolve ) => queueMicrotask ( ( ) => resolve ( ) ) ) ;
1893+
1894+ // Note: sendRemoteMessage catches the timeout error and returns undefined
1895+ // The timeout error is handled internally
1896+ expect ( await sendPromise ) . toBeUndefined ( ) ;
1897+
1898+ // Verify that writeWithTimeout was called (the timeout error message includes the duration)
1899+ expect ( mockChannel . msgStream . write ) . toHaveBeenCalled ( ) ;
1900+ } ) ;
1901+
1902+ it ( 'handles multiple concurrent writes with timeout' , async ( ) => {
1903+ // Ensure isReconnecting returns false so we actually call writeWithTimeout
1904+ mockReconnectionManager . isReconnecting . mockReturnValue ( false ) ;
1905+
1906+ const mockChannel = createMockChannel ( 'peer-1' ) ;
1907+ // Make write hang indefinitely - return a new hanging promise each time
1908+ mockChannel . msgStream . write . mockReset ( ) ;
1909+ mockChannel . msgStream . write . mockImplementation (
1910+ async ( ) =>
1911+ new Promise < never > ( ( ) => {
1912+ // Never resolves - simulates hanging write
1913+ } ) ,
1914+ ) ;
1915+ mockConnectionFactory . dialIdempotent . mockResolvedValue ( mockChannel ) ;
1916+
1917+ const mockSignals : ReturnType < typeof makeAbortSignalMock > [ ] = [ ] ;
1918+ vi . spyOn ( AbortSignal , 'timeout' ) . mockImplementation ( ( ms : number ) => {
1919+ const signal = makeAbortSignalMock ( ms ) ;
1920+ mockSignals . push ( signal ) ;
1921+ return signal ;
1922+ } ) ;
1923+
1924+ const { sendRemoteMessage } = await initNetwork ( '0x1234' , { } , vi . fn ( ) ) ;
1925+
1926+ const sendPromise1 = sendRemoteMessage ( 'peer-1' , 'message 1' ) ;
1927+ const sendPromise2 = sendRemoteMessage ( 'peer-1' , 'message 2' ) ;
1928+
1929+ // Wait for the promises to be set up and event listeners registered
1930+ await new Promise < void > ( ( resolve ) => queueMicrotask ( ( ) => resolve ( ) ) ) ;
1931+
1932+ // Manually trigger the abort on all signals to simulate timeout
1933+ for ( const signal of mockSignals ) {
1934+ signal . abort ( ) ;
1935+ }
1936+
1937+ // Wait for the abort handlers to execute
1938+ await new Promise < void > ( ( resolve ) => queueMicrotask ( ( ) => resolve ( ) ) ) ;
1939+
1940+ // Note: sendRemoteMessage catches the timeout error and returns undefined
1941+ // The timeout error is handled internally
1942+ expect ( await sendPromise1 ) . toBeUndefined ( ) ;
1943+ expect ( await sendPromise2 ) . toBeUndefined ( ) ;
1944+
1945+ // Verify that writeWithTimeout was called for both messages
1946+ expect ( mockChannel . msgStream . write ) . toHaveBeenCalledTimes ( 2 ) ;
1947+ } ) ;
1948+ } ) ;
17021949} ) ;
0 commit comments