current commit
260708.10 · stream: use ring buffer for WHATWG stream queues
'use strict';

const common = require('../common');
const assert = require('assert');

// Regression test for the ring-buffer [[queue]] backing store used by the
// WHATWG stream controllers: exercises growth, wrap-around, drain-rewind
// and shrink behavior through the public API only.

async function testDeepDefaultQueue() {
  // Queue 3000 chunks before reading anything: the backing ring must grow
  // (and re-linearize) repeatedly, then fully drain (shrink on rewind).
  const rs = new ReadableStream({
    start(controller) {
      for (let i = 0; i < 3000; i++)
        controller.enqueue(i);
      controller.close();
    },
  }, { highWaterMark: Infinity });

  const reader = rs.getReader();
  for (let i = 0; i < 3000; i++) {
    const { value, done } = await reader.read();
    assert.strictEqual(done, false);
    assert.strictEqual(value, i);
  }
  const { done } = await reader.read();
  assert.strictEqual(done, true);
}

async function testInterleavedWraparound() {
  // Interleave enqueues and reads so head/tail wrap around the ring many
  // times without ever growing it, including repeated empty transitions.
  let enqueued = 0;
  let controller;
  const rs = new ReadableStream({
    start(c) {
      controller = c;
    },
  });

  const reader = rs.getReader();
  let expected = 0;
  for (let round = 0; round < 500; round++) {
    const burst = (round % 3) + 1;
    for (let i = 0; i < burst; i++)
      controller.enqueue(enqueued++);
    for (let i = 0; i < burst; i++) {
      const { value, done } = await reader.read();
      assert.strictEqual(done, false);
      assert.strictEqual(value, expected++);
    }
  }
  assert.strictEqual(expected, enqueued);
}

async function testCustomSizesSurviveQueueing() {
  // Fractional and zero sizes must round-trip through the queue exactly
  // (desiredSize accounting depends on the stored per-chunk size).
  const sizes = [0.25, 0, 1.75, 3, 0.5];
  const chunks = ['a', 'b', 'c', 'd', 'e'];
  let controller;
  const rs = new ReadableStream({
    start(c) {
      controller = c;
    },
  }, {
    highWaterMark: 10,
    size(chunk) {
      return sizes[chunks.indexOf(chunk)];
    },
  });

  let expectedDesired = 10;
  for (let i = 0; i < chunks.length; i++) {
    controller.enqueue(chunks[i]);
    expectedDesired -= sizes[i];
    assert.strictEqual(controller.desiredSize, expectedDesired);
  }
  const reader = rs.getReader();
  for (let i = 0; i < chunks.length; i++) {
    const { value } = await reader.read();
    assert.strictEqual(value, chunks[i]);
    expectedDesired += sizes[i];
    assert.strictEqual(controller.desiredSize, expectedDesired);
  }
}

async function testByteQueueUnevenReads() {
  // Enqueue 7-byte chunks but read through 4-byte BYOB views: the byte
  // queue's head record is partially consumed in place across reads.
  const total = 7 * 300;
  let written = 0;
  const rs = new ReadableStream({
    type: 'bytes',
    pull(controller) {
      if (written >= total) {
        controller.close();
        return;
      }
      const chunk = new Uint8Array(7);
      for (let i = 0; i < 7; i++)
        chunk[i] = (written + i) % 251;
      written += 7;
      controller.enqueue(chunk);
    },
  });

  const reader = rs.getReader({ mode: 'byob' });
  let read = 0;
  while (read < total) {
    const { value, done } = await reader.read(new Uint8Array(4));
    if (done) break;
    for (let i = 0; i < value.length; i++) {
      assert.strictEqual(value[i], (read + i) % 251,
                         `byte ${read + i} mismatch`);
    }
    read += value.length;
  }
  assert.strictEqual(read, total);
}

async function testDeepByteQueueBurst() {
  // Fill the byte queue with 2500 records before the default reader
  // drains it, forcing ring growth and the drain-to-empty rewind.
  const rs = new ReadableStream({
    type: 'bytes',
    start(controller) {
      for (let i = 0; i < 2500; i++)
        controller.enqueue(new Uint8Array([i & 0xff]));
      controller.close();
    },
  });

  const reader = rs.getReader();
  for (let i = 0; i < 2500; i++) {
    const { value, done } = await reader.read();
    assert.strictEqual(done, false);
    assert.strictEqual(value[0], i & 0xff);
  }
  const { done } = await reader.read();
  assert.strictEqual(done, true);
}

async function testWritableQueueBackpressureDrain() {
  // Buffer thousands of writes behind a slow sink, then let it drain:
  // the writable controller queue grows, wraps, and empties while every
  // chunk is delivered in order.
  const received = [];
  let release;
  const gate = new Promise((resolve) => { release = resolve; });
  const ws = new WritableStream({
    async write(chunk) {
      if (received.length === 0) await gate;
      received.push(chunk);
    },
  }, { highWaterMark: 1 });

  const writer = ws.getWriter();
  const writes = [];
  for (let i = 0; i < 2000; i++)
    writes.push(writer.write(i));
  release();
  await writer.close();
  await Promise.all(writes);
  assert.strictEqual(received.length, 2000);
  for (let i = 0; i < 2000; i++)
    assert.strictEqual(received[i], i);
}

(async () => {
  await testDeepDefaultQueue();
  await testInterleavedWraparound();
  await testCustomSizesSurviveQueueing();
  await testByteQueueUnevenReads();
  await testDeepByteQueueBurst();
  await testWritableQueueBackpressureDrain();
})().then(common.mustCall());