current commit
260906.11 · stream: ensure that stateful transforms preserve this
// Flags: --experimental-stream-iter
'use strict';

const common = require('../common');
const assert = require('assert');
const {
  pullSync,
  fromSync,
  bytesSync,
  tapSync,
  toStreamable,
} = require('stream/iter');

function testPullSyncIdentity() {
  // No transforms - just pass through
  const data = bytesSync(pullSync(fromSync('hello')));
  assert.deepStrictEqual(data, new TextEncoder().encode('hello'));
}

function testPullSyncNormalizesSourceAtCallTime() {
  let protocolCalls = 0;
  let iteratorCalls = 0;
  const source = {
    [toStreamable]() {
      protocolCalls++;
      return {
        *[Symbol.iterator]() {
          iteratorCalls++;
          yield 'data';
        },
      };
    },
  };

  const result = pullSync(source);
  assert.strictEqual(protocolCalls, 1);
  assert.strictEqual(iteratorCalls, 0);
  assert.strictEqual(new TextDecoder().decode(bytesSync(result)), 'data');
  assert.strictEqual(protocolCalls, 1);
  assert.strictEqual(iteratorCalls, 1);
  assert.throws(() => pullSync(null), { code: 'ERR_INVALID_ARG_TYPE' });
}

function testPullSyncStatelessTransform() {
  const upper = (chunks) => {
    if (chunks === null) return null;
    return chunks.map((c) => {
      const str = new TextDecoder().decode(c);
      return new TextEncoder().encode(str.toUpperCase());
    });
  };
  const data = bytesSync(pullSync(fromSync('abc'), upper));
  assert.deepStrictEqual(data, new TextEncoder().encode('ABC'));
}

function testPullSyncStatefulTransform() {
  const source = fromSync('data');
  const stateful = {
    transform: function*(source) {
      for (const chunks of source) {
        if (chunks === null) {
          // Flush: emit trailer
          yield new TextEncoder().encode('-END');
          continue;
        }
        for (const chunk of chunks) {
          yield chunk;
        }
      }
    },
  };
  const result = pullSync(source, stateful);
  const data = new TextDecoder().decode(bytesSync(result));
  assert.strictEqual(data, 'data-END');
}

function testPullSyncStatefulTransformReceiver() {
  const descriptor = {};
  descriptor.transform = common.mustCall(
    function*(source) {
      assert.strictEqual(this, descriptor);
      yield* source;
    });

  assert.strictEqual(
    new TextDecoder().decode(bytesSync(pullSync(fromSync('receiver'), descriptor))),
    'receiver',
  );
}

function testPullSyncChainedTransforms() {
  const addExcl = (chunks) => {
    if (chunks === null) return null;
    return [...chunks, new TextEncoder().encode('!')];
  };
  const addQ = (chunks) => {
    if (chunks === null) return null;
    return [...chunks, new TextEncoder().encode('?')];
  };
  const result = pullSync(fromSync('hello'), addExcl, addQ);
  const data = new TextDecoder().decode(bytesSync(result));
  assert.strictEqual(data, 'hello!?');
}

// PullSync source error propagates
function testPullSyncSourceError() {
  function* failingSource() {
    yield [new TextEncoder().encode('a')];
    throw new Error('sync source boom');
  }
  assert.throws(() => {
    // eslint-disable-next-line no-unused-vars
    for (const _ of pullSync(failingSource())) { /* consume */ }
  }, { message: 'sync source boom' });
}

// PullSync with empty source
function testPullSyncEmptySource() {
  function* empty() {}
  const result = bytesSync(pullSync(empty()));
  assert.strictEqual(result.length, 0);
}

// TapSync callback error propagates
function testTapSyncCallbackError() {
  const badTap = tapSync(() => { throw new Error('tapSync boom'); });
  assert.throws(() => {
    // eslint-disable-next-line no-unused-vars
    for (const _ of pullSync(fromSync('hello'), badTap)) { /* consume */ }
  }, { message: 'tapSync boom' });
}

// Stateless transform error propagates
function testPullSyncStatelessTransformError() {
  const badTransform = (chunks) => {
    if (chunks === null) return null;
    throw new Error('stateless transform boom');
  };
  assert.throws(() => {
    // eslint-disable-next-line no-unused-vars
    for (const _ of pullSync(fromSync('hello'), badTransform)) { /* consume */ }
  }, { message: 'stateless transform boom' });
}

// Stateful transform error propagates
function testPullSyncStatefulTransformError() {
  const badStateful = {
    transform: function*(source) { // eslint-disable-line require-yield
      for (const chunks of source) {
        if (chunks === null) continue;
        throw new Error('stateful transform boom');
      }
    },
  };
  assert.throws(() => {
    // eslint-disable-next-line no-unused-vars
    for (const _ of pullSync(fromSync('hello'), badStateful)) { /* consume */ }
  }, { message: 'stateful transform boom' });
}

// Stateless transform flush emitting data
function testPullSyncStatelessTransformFlush() {
  const withTrailer = (chunks) => {
    if (chunks === null) {
      // Flush: emit trailing data
      return [new TextEncoder().encode('-TRAILER')];
    }
    return chunks;
  };
  const data = new TextDecoder().decode(bytesSync(pullSync(fromSync('data'), withTrailer)));
  assert.strictEqual(data, 'data-TRAILER');
}

// Consecutive stateless transforms each receive a final flush signal after
// upstream flush output has been processed.
function testPullSyncConsecutiveStatelessTransformFlush() {
  const enc = new TextEncoder();
  const addAOnFlush = (chunks) => (chunks === null ?
    [enc.encode('-A')] : chunks);
  const addBOnFlush = (chunks) => (chunks === null ?
    [enc.encode('-B')] : chunks);

  const data = new TextDecoder().decode(bytesSync(
    pullSync(fromSync('x'), addAOnFlush, addBOnFlush)));
  assert.strictEqual(data, 'x-A-B');
}

// Stateless transform flush error propagates
function testPullSyncStatelessTransformFlushError() {
  const badFlush = (chunks) => {
    if (chunks === null) {
      throw new Error('flush boom');
    }
    return chunks;
  };
  assert.throws(() => {
    // eslint-disable-next-line no-unused-vars
    for (const _ of pullSync(fromSync('hello'), badFlush)) { /* consume */ }
  }, { message: 'flush boom' });
}

// Empty source result is a Uint8Array
function testPullSyncEmptySourceType() {
  function* empty() {}
  const result = bytesSync(pullSync(empty()));
  assert.ok(result instanceof Uint8Array);
  assert.strictEqual(result.byteLength, 0);
}

// Invalid transform argument
function testPullSyncInvalidTransform() {
  assert.throws(
    () => { for (const _ of pullSync(fromSync('x'), 42)) { /* consume */ } },  // eslint-disable-line no-unused-vars
    { code: 'ERR_INVALID_ARG_TYPE' },
  );
  assert.throws(
    () => { for (const _ of pullSync(fromSync('x'), null)) { /* consume */ } },  // eslint-disable-line no-unused-vars
    { code: 'ERR_INVALID_ARG_TYPE' },
  );
}

Promise.all([
  testPullSyncIdentity(),
  testPullSyncNormalizesSourceAtCallTime(),
  testPullSyncStatelessTransform(),
  testPullSyncStatefulTransform(),
  testPullSyncStatefulTransformReceiver(),
  testPullSyncChainedTransforms(),
  testPullSyncSourceError(),
  testPullSyncEmptySource(),
  testPullSyncEmptySourceType(),
  testTapSyncCallbackError(),
  testPullSyncStatelessTransformError(),
  testPullSyncStatefulTransformError(),
  testPullSyncStatelessTransformFlush(),
  testPullSyncConsecutiveStatelessTransformFlush(),
  testPullSyncStatelessTransformFlushError(),
  testPullSyncInvalidTransform(),
]).then(common.mustCall());