current commit
261015.9 · stream: create stream/iter iterator results with a constructor
// Flags: --experimental-stream-iter
'use strict';

const common = require('../common');
const assert = require('assert');
const { setTimeout, setImmediate } = require('timers/promises');
const { broadcast, dump, text } = require('stream/iter');

// =============================================================================
// Basic broadcast
// =============================================================================

async function testBasicBroadcast() {
  const { writer, broadcast: bc } = broadcast();

  // Create two consumers
  const consumer1 = bc.push();
  const consumer2 = bc.push();

  assert.strictEqual(bc.consumerCount, 2);

  const dataPromise = Promise.all([
    text(consumer1),
    text(consumer2),
  ]);
  await writer.write('hello');
  await writer.end();

  const [data1, data2] = await dataPromise;

  assert.strictEqual(data1, 'hello');
  assert.strictEqual(data2, 'hello');
}

async function testMultipleWrites() {
  const { writer, broadcast: bc } = broadcast({ budget: 16384 });

  const consumer = bc.push();

  await writer.write('a');
  await writer.write('b');
  await writer.write('c');
  const dataPromise = text(consumer);
  await writer.end();

  const data = await dataPromise;
  assert.strictEqual(data, 'abc');
}

async function testConsumerCount() {
  const { broadcast: bc } = broadcast();

  assert.strictEqual(bc.consumerCount, 0);

  const c1 = bc.push();
  assert.strictEqual(bc.consumerCount, 1);

  bc.push();
  assert.strictEqual(bc.consumerCount, 2);

  bc.cancel();

  // After cancel, consumer count drops to 0
  assert.strictEqual(bc.consumerCount, 0);

  // Consumers are detached and yield nothing
  const batches = [];
  for await (const batch of c1) {
    batches.push(batch);
  }
  assert.strictEqual(batches.length, 0);
}

// =============================================================================
// Writer methods
// =============================================================================

async function testWriteSync() {
  const kChunk = new Uint8Array(16384);
  const { writer, broadcast: bc } = broadcast({ budget: 16384 });
  const consumer = bc.push();

  assert.strictEqual(writer.writeSync(kChunk), true);
  // Buffer full (16384 >= budget), strict policy rejects
  assert.strictEqual(writer.writeSync(kChunk), false);

  writer.endSync();

  const data = await text(consumer);
  assert.strictEqual(data.length, 16384);
}

async function testWritevSync() {
  const { writer, broadcast: bc } = broadcast({ budget: 16384 });
  const consumer = bc.push();

  assert.strictEqual(writer.writevSync(['hello', ' ', 'world']), true);
  writer.endSync();

  const data = await text(consumer);
  assert.strictEqual(data, 'hello world');
}

async function testWriterEnd() {
  const { writer, broadcast: bc } = broadcast();
  const consumer = bc.push();

  await writer.write('data');
  const dataPromise = text(consumer);
  const totalBytes = await writer.end();
  assert.strictEqual(totalBytes, 4); // 'data' = 4 UTF-8 bytes

  const data = await dataPromise;
  assert.strictEqual(data, 'data');
}

async function testWriterEndWithPreAbortedSignal() {
  const { writer, broadcast: bc } = broadcast();
  const consumer = bc.push();
  const reason = new Error('end aborted');

  await assert.rejects(
    writer.end({ signal: AbortSignal.abort(reason) }),
    (error) => error === reason,
  );

  // A rejected end must leave the writer open.
  await writer.write('data');
  const dataPromise = text(consumer);
  assert.strictEqual(await writer.end(), 4);
  assert.strictEqual(await dataPromise, 'data');
}

async function testWriterEndWaitsForAllConsumers() {
  const { writer, broadcast: bc } = broadcast();
  const iter1 = bc.push()[Symbol.asyncIterator]();
  const iter2 = bc.push()[Symbol.asyncIterator]();

  await writer.write('data');
  const endPromise = writer.end();
  let endResolved = false;
  endPromise.then(common.mustCall(() => { endResolved = true; }));

  assert.strictEqual((await iter1.next()).done, false);
  assert.strictEqual((await iter2.next()).done, false);
  assert.strictEqual((await iter1.next()).done, true);
  assert.strictEqual(endResolved, false);

  assert.strictEqual((await iter2.next()).done, true);
  assert.strictEqual(await endPromise, 4);
}

async function testWriterEndSignalDoesNotFailWriter() {
  const { writer, broadcast: bc } = broadcast();
  const consumer = bc.push();
  const ac = new AbortController();
  const reason = new Error('end aborted');

  await writer.write('data');
  const signaledEnd = writer.end({ signal: ac.signal });
  const rejected = assert.rejects(signaledEnd, (error) => error === reason);
  ac.abort(reason);
  await rejected;

  const dataPromise = text(consumer);
  assert.strictEqual(await writer.end(), 4);
  assert.strictEqual(await dataPromise, 'data');
}

async function testWriterFailWhileClosing() {
  const { writer, broadcast: bc } = broadcast();
  const iter = bc.push()[Symbol.asyncIterator]();
  const reason = new Error('writer failed while closing');

  await writer.write('data');
  const endPromise = writer.end();
  const endRejected = assert.rejects(endPromise, (error) => error === reason);
  writer.fail(reason);

  await endRejected;
  await assert.rejects(iter.next(), (error) => error === reason);
}

async function testWriterFail() {
  const { writer, broadcast: bc } = broadcast();
  const consumer = bc.push();

  writer.fail(new Error('test error'));

  await assert.rejects(
    async () => {
      // eslint-disable-next-line no-unused-vars
      for await (const _ of consumer) {
        assert.fail('Should not reach here');
      }
    },
    { message: 'test error' },
  );
}

// =============================================================================
// Cancel
// =============================================================================

async function testCancelWithoutReason() {
  const { broadcast: bc } = broadcast();
  const consumer = bc.push();

  bc.cancel();

  const batches = [];
  for await (const batch of consumer) {
    batches.push(batch);
  }
  assert.strictEqual(batches.length, 0);
}

async function testCancelWithReason() {
  const { broadcast: bc } = broadcast();

  // Start a consumer that is waiting for data (promise pending)
  const consumer = bc.push();
  const resultPromise = text(consumer).catch((err) => err);

  // Give the consumer time to enter the waiting state
  await setImmediate();

  bc.cancel(new Error('cancelled'));

  const result = await resultPromise;
  assert.ok(result instanceof Error);
  assert.strictEqual(result.message, 'cancelled');
}

async function testPendingNextSettlesAfterReturn() {
  const { broadcast: bc } = broadcast();
  const iter = bc.push()[Symbol.asyncIterator]();

  const pendingNext = iter.next();
  await iter.return();

  const result = await pendingNext;
  assert.strictEqual(result.done, true);
  assert.strictEqual(result.value, undefined);
}

async function testPushAbortSignalRejectsPendingNext() {
  const ac = new AbortController();
  const reason = new Error('push aborted');
  const { broadcast: bc } = broadcast();
  const iter = bc.push({ signal: ac.signal })[Symbol.asyncIterator]();

  const pendingNext = iter.next();
  const rejected = assert.rejects(pendingNext, (error) => error === reason);
  ac.abort(reason);

  await rejected;
}

async function testPushPreAbortedSignalDoesNotAddConsumer() {
  const reason = new Error('already aborted');
  const signal = AbortSignal.abort(reason);
  const { broadcast: bc } = broadcast();
  const iter = bc.push({ signal })[Symbol.asyncIterator]();

  assert.strictEqual(bc.consumerCount, 0);
  await assert.rejects(iter.next(), (error) => error === reason);
  assert.strictEqual(bc.consumerCount, 0);
}

// =============================================================================
// Writer fail detaches consumers
// =============================================================================

async function testFailDetachesConsumers() {
  const { writer, broadcast: bc } = broadcast();
  const consumer1 = bc.push();
  const consumer2 = bc.push();

  assert.strictEqual(bc.consumerCount, 2);

  // Write some data, then fail the writer
  await writer.write('data');
  await writer.fail(new Error('writer failed'));

  // After fail, consumers are detached
  assert.strictEqual(bc.consumerCount, 0);

  // Both consumers should see the error
  await assert.rejects(
    async () => {
      // eslint-disable-next-line no-unused-vars
      for await (const _ of consumer1) {
        assert.fail('Should not reach here');
      }
    },
    { message: 'writer failed' },
  );

  await assert.rejects(
    async () => {
      // eslint-disable-next-line no-unused-vars
      for await (const _ of consumer2) {
        assert.fail('Should not reach here');
      }
    },
    { message: 'writer failed' },
  );
}

// =============================================================================
// Writer fail idempotent
// =============================================================================

async function testWriterFailIdempotent() {
  const { writer, broadcast: bc } = broadcast();
  const consumer = bc.push();
  writer.writeSync('hello');
  writer.fail(new Error('fail!'));
  // Second call is a no-op (already errored)
  writer.fail(new Error('fail2'));
  await assert.rejects(async () => {
    await dump(consumer);
  }, { message: 'fail!' });
}

async function testCancelWithFalsyReason() {
  for (const reason of [0, '', false, null]) {
    const { broadcast: bc } = broadcast();
    const iterator = bc.push()[Symbol.asyncIterator]();

    bc.cancel(reason);

    await assert.rejects(iterator.next(), (error) => error === reason);
  }
}

// Late-joining consumer should read from oldest buffered entry
async function testLateJoinerSeesBufferedData() {
  const { writer, broadcast: bc } = broadcast({ budget: 16384 });

  // Write data before any consumer joins
  writer.writeSync('before-join');
  writer.endSync();

  // Consumer joins after data is written
  const consumer = bc.push();
  const result = await text(consumer);
  assert.strictEqual(result, 'before-join');
}

async function testLateJoinerAfterDetachSeesBufferedData() {
  const { writer, broadcast: bc } = broadcast({ budget: 16384 });
  const first = bc.push()[Symbol.asyncIterator]();

  writer.writeSync('before-detach');
  await first.return();

  const second = bc.push();
  writer.endSync();
  assert.strictEqual(await text(second), 'before-detach');
}

async function testOverlappingNextKeepsEarlierRead() {
  const { writer, broadcast: bc } = broadcast();
  const it = bc.push()[Symbol.asyncIterator]();

  const first = it.next();
  const second = it.next();

  await writer.write('x');

  const secondResult = await Promise.race([
    second.then((value) => ({ __proto__: null, settled: true, value })),
    setTimeout(common.platformTimeout(50),
               { __proto__: null, settled: false }),
  ]);
  assert.deepStrictEqual(secondResult, {
    __proto__: null,
    settled: false,
  });

  const result = await first;
  assert.strictEqual(result.done, false);
  assert.strictEqual(Buffer.concat(result.value).toString(), 'x');

  writer.endSync();
  assert.deepStrictEqual({ ...await second }, {
    done: true,
    value: undefined,
  });
  assert.strictEqual(bc.consumerCount, 0);
}

Promise.all([
  testBasicBroadcast(),
  testMultipleWrites(),
  testConsumerCount(),
  testWriteSync(),
  testWritevSync(),
  testWriterEnd(),
  testWriterEndWithPreAbortedSignal(),
  testWriterEndWaitsForAllConsumers(),
  testWriterEndSignalDoesNotFailWriter(),
  testWriterFailWhileClosing(),
  testWriterFail(),
  testCancelWithoutReason(),
  testCancelWithReason(),
  testCancelWithFalsyReason(),
  testPendingNextSettlesAfterReturn(),
  testPushAbortSignalRejectsPendingNext(),
  testPushPreAbortedSignalDoesNotAddConsumer(),
  testFailDetachesConsumers(),
  testWriterFailIdempotent(),
  testLateJoinerSeesBufferedData(),
  testLateJoinerAfterDetachSeesBufferedData(),
  testOverlappingNextKeepsEarlierRead(),
]).then(common.mustCall());