diff --git a/Iterator.concat/auto.js b/Iterator.concat/auto.js new file mode 100644 index 0000000..8ebf606 --- /dev/null +++ b/Iterator.concat/auto.js @@ -0,0 +1,3 @@ +'use strict'; + +require('./shim')(); diff --git a/Iterator.concat/implementation.js b/Iterator.concat/implementation.js new file mode 100644 index 0000000..339dcec --- /dev/null +++ b/Iterator.concat/implementation.js @@ -0,0 +1,112 @@ +'use strict'; + +var $TypeError = require('es-errors/type'); + +var AdvanceStringIndex = require('es-abstract/2024/AdvanceStringIndex'); +var Call = require('es-abstract/2024/Call'); +var CompletionRecord = require('es-abstract/2024/CompletionRecord'); +var CreateIteratorFromClosure = require('../aos/CreateIteratorFromClosure'); +var GetIteratorDirect = require('../aos/GetIteratorDirect'); +var GetMethod = require('es-abstract/2024/GetMethod'); +var IsArray = require('es-abstract/2024/IsArray'); +var IteratorClose = require('es-abstract/2024/IteratorClose'); +var IteratorStepValue = require('es-abstract/2024/IteratorStepValue'); +var ThrowCompletion = require('es-abstract/2024/ThrowCompletion'); +var Type = require('es-abstract/2024/Type'); + +var forEach = require('es-abstract/helpers/forEach'); +var getIteratorMethod = require('es-abstract/helpers/getIteratorMethod'); + +var iterHelperProto = require('../IteratorHelperPrototype'); + +var SLOT = require('internal-slot'); + +module.exports = function concat() { + if (this instanceof concat) { + throw new $TypeError('`Iterator.concat` is not a constructor'); + } + + var iterables = []; // step 1 + var index = 0; + + forEach(arguments, function (item) { // step 2 + if (Type(item) !== 'Object') { + throw new $TypeError('`Iterator.concat` requires all arguments to be objects'); // step 2.1 + } + // var method = GetMethod(item, Symbol.iterator); // step 2.2 + var method = getIteratorMethod( + { + AdvanceStringIndex: AdvanceStringIndex, + GetMethod: GetMethod, + IsArray: IsArray + }, + item + ); + if (typeof method === 'undefined') { + throw new $TypeError('`Iterator.concat` requires all arguments to be iterable'); // step 2.3 + } + iterables[iterables.length] = { '[[OpenMethod]]': method, '[[Iterable]]': item }; // step 2.4 + }); + + var sentinel = {}; + var innerIterator = sentinel; + var closeIfAbrupt = function (abruptCompletion) { + if (!(abruptCompletion instanceof CompletionRecord)) { + throw new $TypeError('`abruptCompletion` must be a Completion Record'); + } + if (innerIterator !== sentinel) { + IteratorClose( + innerIterator, + abruptCompletion + ); + } + }; + + var closure = function () { // step 3 + if (index < iterables.length) { + // forEach(iterables, function (iterable) { // step 3.a + var iteratorRecord; + if (innerIterator === sentinel) { + var iterable = iterables[index]; + var iter = Call(iterable['[[OpenMethod]]'], iterable['[[Iterable]]']); // step 3.a.i + if (Type(iter) !== 'Object') { + closeIfAbrupt(ThrowCompletion(new $TypeError('???'))); // step 3.a.ii + } + iteratorRecord = GetIteratorDirect(iter); // step 3.a.iii + innerIterator = iteratorRecord; + } else { + iteratorRecord = innerIterator; + } + + // var innerAlive = true; // step 3.a.iv + // while (innerAlive) { // step 3.a.v + if (innerIterator !== sentinel) { + // step 3.a.v.3.a + var innerValue; + try { + innerValue = IteratorStepValue(iteratorRecord); // step 5.b.ix.4.a + } catch (e) { + // innerAlive = false; + innerIterator = sentinel; + index += 1; + closeIfAbrupt(ThrowCompletion(e)); // step 3.a.v.3.b + } + if (iteratorRecord['[[Done]]']) { + // innerAlive = false; + innerIterator = sentinel; + index += 1; + return closure(); + } + return innerValue; // // step 3.a.v.3.a + } + // }); + } + + // return ReturnCompletion(undefined); // step 3.b + return sentinel; + }; + SLOT.set(closure, '[[Sentinel]]', sentinel); // for the userland implementation + SLOT.set(closure, '[[CloseIfAbrupt]]', closeIfAbrupt); // for the userland implementation + + return CreateIteratorFromClosure(closure, 'Iterator Helper', iterHelperProto, []); // step 4 +}; diff --git a/Iterator.concat/index.js b/Iterator.concat/index.js new file mode 100644 index 0000000..4c50b07 --- /dev/null +++ b/Iterator.concat/index.js @@ -0,0 +1,18 @@ +'use strict'; + +var callBind = require('call-bind'); +var define = require('define-properties'); + +var implementation = require('./implementation'); +var getPolyfill = require('./polyfill'); +var shim = require('./shim'); + +var bound = callBind(getPolyfill(), null); + +define(bound, { + getPolyfill: getPolyfill, + implementation: implementation, + shim: shim +}); + +module.exports = bound; diff --git a/Iterator.concat/polyfill.js b/Iterator.concat/polyfill.js new file mode 100644 index 0000000..25bbab6 --- /dev/null +++ b/Iterator.concat/polyfill.js @@ -0,0 +1,9 @@ +'use strict'; + +var implementation = require('./implementation'); + +var $Iterator = require('../Iterator'); + +module.exports = function getPolyfill() { + return typeof $Iterator.concat === 'function' ? $Iterator.concat : implementation; +}; diff --git a/Iterator.concat/shim.js b/Iterator.concat/shim.js new file mode 100644 index 0000000..a45f0d8 --- /dev/null +++ b/Iterator.concat/shim.js @@ -0,0 +1,18 @@ +'use strict'; + +var getPolyfill = require('./polyfill'); +var define = require('define-properties'); + +var getIteratorPolyfill = require('../Iterator/polyfill'); + +module.exports = function shimIteratorConcat() { + var $Iterator = getIteratorPolyfill(); + var polyfill = getPolyfill(); + define( + $Iterator, + { concat: polyfill }, + { concat: function () { return $Iterator.concat !== polyfill; } } + ); + + return polyfill; +}; diff --git a/README.md b/README.md index 1bb73d7..b26a724 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ An ESnext spec-compliant sync iterator helpers shim/polyfill/replacement that works as far down as ES3. -This package implements the [es-shim API](https://github.com/es-shims/api) “multi” interface. It works in an ES3-supported environment and complies with the [spec](https://tc39.es/proposal-iterator-helpers/). +This package implements the [es-shim API](https://github.com/es-shims/api) “multi” interface. It works in an ES3-supported environment and complies with the [iterator helpers spec](https://tc39.es/proposal-iterator-helpers/) and the [iterator sequencing spec](https://tc39.es/proposal-iterator-sequencing/). Because the `Iterator.prototype` methods depend on a receiver (the `this` value), the main export in each subdirectory takes the iterator to operate on as the first argument. @@ -19,6 +19,7 @@ The main export of the package itself is simply an array of the available direct - [`Iterator` constructor](https://tc39.es/proposal-iterator-helpers/#sec-iterator-constructor) - [`Iterator.prototype`](https://tc39.es/proposal-iterator-helpers/#sec-iterator.prototype) + - [`Iterator.concat`](https://tc39.es/proposal-iterator-sequencing/) - [`Iterator.from`](https://tc39.es/proposal-iterator-helpers/#sec-iterator.from) - [`Iterator.prototype.constructor`](https://tc39.es/proposal-iterator-helpers/#sec-iteratorprototype.constructor) - [`Iterator.prototype.drop`](https://tc39.es/proposal-iterator-helpers/#sec-iteratorprototype.drop) @@ -37,6 +38,7 @@ The main export of the package itself is simply an array of the available direct - node v22, Chrome >= v122: has a [bug](https://issues.chromium.org/issues/336839115) - node < v22, Chrome < v122, Safari <= v17.1, Firefox <= v125: not implemented + - all environments lack Iterator.concat ## Getting started diff --git a/index.json b/index.json index 0e3ce66..9ac26be 100644 --- a/index.json +++ b/index.json @@ -1,5 +1,6 @@ [ "Iterator", + "Iterator.concat", "Iterator.from", "Iterator.prototype", "Iterator.prototype.constructor", diff --git a/package.json b/package.json index 2ae89a6..f4e3659 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,11 @@ "./Iterator.prototype/polyfill": "./Iterator.prototype/polyfill.js", "./Iterator.prototype/implementation": "./Iterator.prototype/implementation.js", "./Iterator.prototype/shim": "./Iterator.prototype/shim.js", + "./Iterator.concat": "./Iterator.concat/index.js", + "./Iterator.concat/auto": "./Iterator.concat/auto.js", + "./Iterator.concat/polyfill": "./Iterator.concat/polyfill.js", + "./Iterator.concat/implementation": "./Iterator.concat/implementation.js", + "./Iterator.concat/shim": "./Iterator.concat/shim.js", "./Iterator.from": "./Iterator.from/index.js", "./Iterator.from/auto": "./Iterator.from/auto.js", "./Iterator.from/polyfill": "./Iterator.from/polyfill.js", diff --git a/shim.js b/shim.js index 2ba8c91..077f751 100644 --- a/shim.js +++ b/shim.js @@ -2,6 +2,7 @@ var shimIterator = require('./Iterator/shim'); var shimIteratorFrom = require('./Iterator.from/shim'); +var shimIteratorConcat = require('./Iterator.concat/shim'); var shimIteratorProto = require('./Iterator.prototype/shim'); var shimIteratorCtor = require('./Iterator.prototype.constructor/shim'); var shimIteratorDrop = require('./Iterator.prototype.drop/shim'); @@ -19,6 +20,7 @@ var shimIteratorToArray = require('./Iterator.prototype.toArray/shim'); module.exports = function shimIteratorHelpers() { shimIterator(); shimIteratorFrom(); + shimIteratorConcat(); shimIteratorProto(); shimIteratorCtor(); shimIteratorDrop(); diff --git a/test/Iterator.concat.js b/test/Iterator.concat.js new file mode 100644 index 0000000..a244c2b --- /dev/null +++ b/test/Iterator.concat.js @@ -0,0 +1,129 @@ +'use strict'; + +var defineProperties = require('define-properties'); +var test = require('tape'); +var callBind = require('call-bind'); +var functionsHaveNames = require('functions-have-names')(); +var forEach = require('for-each'); +var debug = require('object-inspect'); +var v = require('es-value-fixtures'); +var hasSymbols = require('has-symbols/shams')(); +var mockProperty = require('mock-property'); + +var index = require('../Iterator.concat'); +var impl = require('../Iterator.concat/implementation'); +var from = require('../Iterator.from/polyfill')(); + +var isEnumerable = Object.prototype.propertyIsEnumerable; + +var testIterator = require('./helpers/testIterator'); + +module.exports = { + tests: function (concat, name, t) { + t['throws']( + function () { return new concat(); }, // eslint-disable-line new-cap + TypeError, + '`' + name + '` itself is not a constructor' + ); + t['throws']( + function () { return new concat({}); }, // eslint-disable-line new-cap + TypeError, + '`' + name + '` itself is not a constructor, with an argument' + ); + + forEach(v.primitives.concat(v.objects), function (nonIterator) { + if (typeof nonIterator !== 'string') { + t['throws']( + function () { concat(nonIterator).next(); }, + TypeError, + debug(nonIterator) + ' is not an iterable Object' + ); + } + }); + + t.test('actual iteration', { skip: !hasSymbols }, function (st) { + forEach(v.nonFunctions, function (nonFunction) { + var badIterable = {}; + badIterable[Symbol.iterator] = nonFunction; + st['throws']( + function () { concat([], badIterable, []).next(); }, + TypeError, + debug(badIterable) + ' is not a function' + ); + }); + + forEach(v.strings, function (string) { + st['throws']( + function () { concat(string); }, + TypeError, + 'non-objects are not considered iterable' + ); + var stringIt = concat(['a'], [string], ['c']); + testIterator(stringIt, ['a', string, 'c'], st, 'string iterator: ' + debug(string)); + }); + + var arrayIt = concat([1, 2, 3]); + st.equal(typeof arrayIt.next, 'function', 'has a `next` function'); + + st.test('real iterators', { skip: !hasSymbols }, function (s2t) { + var iter = [1, 2][Symbol.iterator](); + testIterator(concat(iter, [3]), [1, 2, 3], s2t, 'array iterator + array yields combined results'); + + s2t.end(); + }); + + st.test('observability in a replaced String iterator', function (s2t) { + var originalStringIterator = String.prototype[Symbol.iterator]; + var observedType; + s2t.teardown(mockProperty(String.prototype, Symbol.iterator, { + get: function () { + 'use strict'; // eslint-disable-line strict, lines-around-directive + + observedType = typeof this; + return originalStringIterator; + } + })); + + concat(from('')); + s2t.equal(observedType, 'string', 'string primitive -> primitive receiver in Symbol.iterator getter'); + concat(from(Object(''))); + s2t.equal(observedType, 'object', 'boxed string -> boxed string in Symbol.iterator getter'); + + s2t.end(); + }); + + st.end(); + }); + }, + index: function () { + test('Iterator.concat: index', function (t) { + module.exports.tests(index, 'Iterator.concat', t); + + t.end(); + }); + }, + implementation: function () { + test('Iterator.concat: implementation', function (t) { + module.exports.tests(impl, 'Iterator.concat', t); + + t.end(); + }); + }, + shimmed: function () { + test('Iterator.concat: shimmed', function (t) { + t.test('Function name', { skip: !functionsHaveNames }, function (st) { + st.equal(Iterator.concat.name, 'concat', 'Iterator.concat has name "concat"'); + st.end(); + }); + + t.test('enumerability', { skip: !defineProperties.supportsDescriptors }, function (et) { + et.equal(false, isEnumerable.call(Iterator, 'concat'), 'Iterator.concat is not enumerable'); + et.end(); + }); + + module.exports.tests(callBind(Iterator.concat, Iterator), 'Iterator.concat', t); + + t.end(); + }); + } +};