current commit
260924.18 · test: avoid using promise wrapped timers
// Flags: --experimental-stream-iter
'use strict';

const common = require('../common');
const assert = require('assert');
const {
  from,
  fromSync,
  share,
  Share,
  SyncShare,
  text,
  textSync,
} = require('stream/iter');

const { setImmediate } = require('timers/promises');

// =============================================================================
// Share.from
// =============================================================================

async function testShareFrom() {
  const shared = Share.from(from('share-from'));
  const consumer = shared.pull();

  const data = await text(consumer);
  assert.strictEqual(data, 'share-from');
}

function testShareFromRejectsNonStreamable() {
  assert.throws(
    () => Share.from(12345),
    { code: 'ERR_INVALID_ARG_TYPE' },
  );
}

// =============================================================================
// SyncShare.fromSync
// =============================================================================

async function testSyncShareFromSync() {
  const shared = SyncShare.fromSync(fromSync('sync-share-from'));
  const consumer = shared.pull();

  const data = textSync(consumer);
  assert.strictEqual(data, 'sync-share-from');
}

function testSyncShareFromRejectsNonStreamable() {
  assert.throws(
    () => SyncShare.fromSync(12345),
    { code: 'ERR_INVALID_ARG_TYPE' },
  );
}

// =============================================================================
// Protocol validation
// =============================================================================

function testShareProtocolReturnsNull() {
  const obj = {
    [Symbol.for('Stream.shareProtocol')]() { return null; },
  };
  assert.throws(
    () => Share.from(obj),
    { code: 'ERR_INVALID_RETURN_VALUE' },
  );
}

function testShareProtocolReturnsNonObject() {
  const obj = {
    [Symbol.for('Stream.shareProtocol')]() { return 42; },
  };
  assert.throws(
    () => Share.from(obj),
    { code: 'ERR_INVALID_RETURN_VALUE' },
  );
}

function testSyncShareProtocolReturnsNull() {
  const obj = {
    [Symbol.for('Stream.shareSyncProtocol')]() { return null; },
  };
  assert.throws(
    () => SyncShare.fromSync(obj),
    { code: 'ERR_INVALID_RETURN_VALUE' },
  );
}

function testSyncShareProtocolReturnsNonObject() {
  const obj = {
    [Symbol.for('Stream.shareSyncProtocol')]() { return 'bad'; },
  };
  assert.throws(
    () => SyncShare.fromSync(obj),
    { code: 'ERR_INVALID_RETURN_VALUE' },
  );
}

// =============================================================================
// Unbounded backpressure: two consumers, slow consumer blocks the source
// =============================================================================

async function testShareBlockBackpressure() {
  // A source that yields 5 items. With two consumers and budget: 16384,
  // the fast consumer drives the source forward. The slow consumer holds back
  // trimming, causing the buffer to fill. 'unbounded' mode should stall the
  // source pull until the slow consumer catches up.
  const enc = new TextEncoder();
  async function* source() {
    for (let i = 0; i < 5; i++) {
      yield [enc.encode(`item${i}`)];
    }
  }
  const shared = share(source(), { budget: 16384, backpressure: 'unbounded' });
  const fast = shared.pull();
  const slow = shared.pull();

  // Both consumers should ultimately receive all 5 items
  const [fastData, slowData] = await Promise.all([
    text(fast),
    text(slow),
  ]);

  assert.strictEqual(fastData, 'item0item1item2item3item4');
  assert.strictEqual(slowData, 'item0item1item2item3item4');
}

// =============================================================================
// Drop backpressure modes: use a fast + stalled consumer to trigger drops
// =============================================================================

async function testShareDropOldest() {
  // Two consumers, fast reads eagerly then slow reads. With drop-oldest,
  // the slow consumer's cursor is advanced past dropped items, so it
  // misses old data and only sees recent items.
  async function* source() {
    for (let i = 0; i < 4; i++) {
      const chunk = new Uint8Array(16384);
      chunk[0] = i; // Tag first byte with index
      yield [chunk];
    }
  }
  const shared = share(source(), { budget: 32768, backpressure: 'drop-oldest' });
  const fast = shared.pull();
  const slow = shared.pull();

  // Fast consumer reads all items
  const fastIndices = [];
  for await (const batch of fast) {
    for (const chunk of batch) {
      fastIndices.push(chunk[0]);
    }
  }
  assert.strictEqual(fastIndices.length, 4);

  // Slow consumer reads after fast is done — old items were dropped
  const slowIndices = [];
  for await (const batch of slow) {
    for (const chunk of batch) {
      slowIndices.push(chunk[0]);
    }
  }
  // The slow consumer should see fewer items than were produced
  assert.ok(slowIndices.length < 4,
            `Expected < 4 items after drop-oldest, got ${slowIndices.length}`);
  assert.ok(slowIndices.length > 0,
            'Expected at least some items after drop-oldest');
  // The last item should always be present (most recent items kept)
  assert.strictEqual(slowIndices[slowIndices.length - 1], 3);
}

async function testShareDropNewest() {
  let pulls = 0;
  let secondPull;
  const secondPullStarted = new Promise((resolve) => {
    secondPull = resolve;
  });

  async function* source() {
    for (let i = 0; i < 7; i++) {
      pulls++;
      if (pulls === 2) secondPull();
      const chunk = new Uint8Array(16384);
      chunk[0] = i;
      yield [chunk];
    }
  }
  const shared = share(source(), {
    budget: 16384,
    backpressure: 'drop-newest',
  });
  const fast = shared.pull()[Symbol.asyncIterator]();
  const slow = shared.pull()[Symbol.asyncIterator]();

  const first = await fast.next();
  assert.strictEqual(first.value[0][0], 0);

  let nextSettled = false;
  const next = fast.next().then((result) => {
    nextSettled = true;
    return result;
  });

  await secondPullStarted;
  await setImmediate();
  assert.strictEqual(pulls, 2);
  assert.strictEqual(nextSettled, false);

  const slowResult = await slow.next();
  assert.strictEqual(slowResult.value[0][0], 0);

  const nextResult = await next;
  assert.strictEqual(nextResult.value[0][0], 2);
  assert.strictEqual(pulls, 3);

  shared.cancel();
}

// =============================================================================
// Strict backpressure: should throw when buffer overflows
// =============================================================================

async function testShareStrictBackpressure() {
  for (const transformed of [false, true]) {
    async function* source() {
      for (let i = 0; i < 10; i++) {
        yield [new Uint8Array(16384)];
      }
    }
    const shared = share(source(), {
      budget: 32768,
      backpressure: 'strict',
    });
    const consumer = transformed ?
      shared.pull((chunks) => chunks) : shared.pull();
    const fast = consumer[Symbol.asyncIterator]();
    // This consumer prevents the buffer from being trimmed.
    shared.pull();

    await fast.next();
    await fast.next();
    await assert.rejects(fast.next(), { code: 'ERR_OUT_OF_RANGE' });
    assert.strictEqual(shared.consumerCount, 1);
    assert.strictEqual((await fast.next()).done, true);
    shared.cancel();
  }
}

Promise.all([
  testShareFrom(),
  testShareFromRejectsNonStreamable(),
  testSyncShareFromSync(),
  testSyncShareFromRejectsNonStreamable(),
  testShareProtocolReturnsNull(),
  testShareProtocolReturnsNonObject(),
  testSyncShareProtocolReturnsNull(),
  testSyncShareProtocolReturnsNonObject(),
  testShareBlockBackpressure(),
  testShareDropOldest(),
  testShareDropNewest(),
  testShareStrictBackpressure(),
]).then(common.mustCall());