All files / cli/test/dev-server ws-helpers.js

75.75% Statements 25/33
36.36% Branches 4/11
85.71% Functions 6/7
79.31% Lines 23/29

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87                                                          5x 5x 5x   5x   8x 8x   8x 8x 8x 8x 8x 8x 8x           5x   8x 8x         8x 8x               8x 8x                         5x 5x 5x      
/**
 * WebSocket test helpers for the dev server suite.
 *
 * Shared so both hmr-server.test.js and integration.test.js observe sockets
 * the same way — the listen-after-await race below is easy to reintroduce by
 * hand-rolling a one-off waiter.
 */
 
/**
 * Attach a message buffer to a WebSocket the moment it is created so we never
 * miss messages that arrive before a later `await` returns. Returns a
 * `next(predicate)` helper that resolves with the next — or already buffered —
 * matching message.
 *
 * This avoids the classic listen-after-await race. `ws` emits 'message' on an
 * EventEmitter, which does not buffer, and the server sends its `connected`
 * ack on setTimeout(0) right after the handshake. When the handshake response
 * and the ack land in the same client-side read, `ws` emits 'open' and then
 * 'message' within one macrotask — before the continuation of
 * `await waitForOpen(ws)` has run to attach a listener. The ack is dropped and
 * the test waits for a message that already came and went. Subscribing at
 * construction removes the race rather than narrowing it; no server-side
 * deferral can close it, because the server cannot know when the client
 * subscribes.
 *
 * @param {object} ws - Socket to observe, freshly constructed.
 * @returns {{ next: (predicate: (data: any) => boolean, timeoutMs?: number) => Promise<any> }}
 */
export function bufferMessages(ws) {
  const buffer = [];
  const waiters = [];
  const seenTypes = [];
 
  ws.on('message', (buf) => {
    let data;
    try { data = JSON.parse(buf.toString()); } catch { return; }
    seenTypes.push(data?.type ?? '(untyped)');
 
    for (let i = waiters.length - 1; i >= 0; i--) {
      const waiter = waiters[i];
      Eif (waiter.predicate(data)) {
        waiters.splice(i, 1);
        clearTimeout(waiter.timer);
        waiter.resolve(data);
        return;
      }
    }
    buffer.push(data);
  });
 
  return {
    next(predicate, timeoutMs = 4000) {
      const match = buffer.findIndex(predicate);
      Iif (match >= 0) {
        const [data] = buffer.splice(match, 1);
        return Promise.resolve(data);
      }
 
      return new Promise((resolve, reject) => {
        const timer = setTimeout(() => {
          const i = waiters.indexOf(entry);
          if (i >= 0) waiters.splice(i, 1);
          reject(new Error(
            `timeout waiting for message after ${timeoutMs}ms ` +
            `(received: ${seenTypes.length ? seenTypes.join(', ') : 'nothing'})`
          ));
        }, timeoutMs);
        const entry = { predicate, resolve, timer };
        waiters.push(entry);
      });
    },
  };
}
 
/**
 * Resolve once the socket is open, reject if it errors first.
 *
 * @param {object} ws - Socket to await.
 * @returns {Promise<void>}
 */
export function waitForOpen(ws) {
  return new Promise((resolve, reject) => {
    ws.once('open', resolve);
    ws.once('error', reject);
  });
}