├── .gitignore ├── .travis.yml ├── example.js ├── package.json ├── to-stream.js ├── LICENSE ├── README.md ├── index.js └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - 6 5 | - 8 6 | - 10 7 | - 12 8 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var nanoiterator = require('./') 2 | 3 | var values = [1, 2, 3, 4, null] 4 | var ite = nanoiterator({ 5 | next: cb => process.nextTick(cb, null, values.shift()) 6 | }) 7 | 8 | ite.next(console.log) // 1 9 | ite.next(console.log) // 2 10 | ite.next(console.log) // 3 11 | ite.next(console.log) // 4 12 | ite.next(console.log) // null 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nanoiterator", 3 | "version": "1.2.1", 4 | "description": "Lightweight and efficient iterators", 5 | "main": "index.js", 6 | "dependencies": { 7 | "inherits": "^2.0.3", 8 | "readable-stream": "^2.3.3" 9 | }, 10 | "devDependencies": { 11 | "standard": "^10.0.3", 12 | "tape": "^4.8.0" 13 | }, 14 | "scripts": { 15 | "test": "standard && tape test.js" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/mafintosh/nanoiterator.git" 20 | }, 21 | "author": "Mathias Buus (@mafintosh)", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/mafintosh/nanoiterator/issues" 25 | }, 26 | "homepage": "https://github.com/mafintosh/nanoiterator" 27 | } 28 | -------------------------------------------------------------------------------- /to-stream.js: -------------------------------------------------------------------------------- 1 | var stream = require('readable-stream') 2 | var inherits = require('inherits') 3 | 4 | module.exports = IteratorStream 5 | 6 | function IteratorStream (ite) { 7 | if (!(this instanceof IteratorStream)) return new IteratorStream(ite) 8 | stream.Readable.call(this, {objectMode: true}) 9 | 10 | this.iterator = ite 11 | this.onread = onread.bind(null, this) 12 | this.destroyed = false 13 | } 14 | 15 | inherits(IteratorStream, stream.Readable) 16 | 17 | IteratorStream.prototype._read = function () { 18 | this.iterator.next(this.onread) 19 | } 20 | 21 | IteratorStream.prototype.destroy = function (err) { 22 | if (this.destroyed) return 23 | this.destroyed = true 24 | 25 | var self = this 26 | 27 | this.iterator.destroy(function (error) { 28 | if (!err) err = error 29 | if (err) self.emit('error', err) 30 | self.emit('close') 31 | }) 32 | } 33 | 34 | function onread (self, err, value) { 35 | if (err) self.destroy(err) 36 | else self.push(value) 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Mathias Buus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nanoiterator 2 | 3 | Lightweight and efficient iterators 4 | 5 | ``` 6 | npm install nanoiterator 7 | ``` 8 | 9 | [![build status](https://travis-ci.org/mafintosh/nanoiterator.svg?branch=master)](https://travis-ci.org/mafintosh/nanoiterator) 10 | 11 | ## Usage 12 | 13 | ``` js 14 | var nanoiterator = require('nanoiterator') 15 | 16 | var values = [1, 2, 3, 4, null] 17 | var ite = nanoiterator({ 18 | next: cb => process.nextTick(cb, null, values.shift()) 19 | }) 20 | 21 | ite.next(console.log) // 1 22 | ite.next(console.log) // 2 23 | ite.next(console.log) // 3 24 | ite.next(console.log) // 4 25 | ite.next(console.log) // null 26 | ``` 27 | 28 | ## API 29 | 30 | #### `var ite = nanoiterator([options])` 31 | 32 | Create a new iterator. 33 | 34 | Options include: 35 | 36 | ``` js 37 | { 38 | open: cb => cb(null), // sets ._open 39 | next: cb => cb(null, nextValue), // sets ._next 40 | destroy: cb => cb(null) // sets ._destroy 41 | } 42 | ``` 43 | 44 | #### `ite.next(callback)` 45 | 46 | Call this function to get the next value from the iterator. It is same to call this 47 | method as many times as you want without waiting for previous calls to finish. 48 | 49 | #### `ite._next(callback)` 50 | 51 | Overwrite this function to your own iteration logic. 52 | 53 | Call `callback(null, nextValue)` when you have a new value to return, or 54 | call `callback(null, null)` if you want to signal that the iterator has ended. 55 | 56 | No matter how many times a user calls `.next(cb)` only *one* `_next` call will 57 | run at the same time. 58 | 59 | #### `ite._open(callback)` 60 | 61 | Optionally overwrite this method with your own open logic. 62 | 63 | Called the first time `._next` is called and is run before the `_next` call runs. 64 | 65 | #### `ite._destroy(callback)` 66 | 67 | Optionally overwrite this method with your own destruction logic. 68 | 69 | Called once when a user calls `.destroy(cb)` and all subsequent `.next()` calls 70 | will result in an error. 71 | 72 | #### `ite.ended` 73 | 74 | Signals if the iterator has been ended (`_next` has returned `(null, null)`). 75 | 76 | #### `ite.opened` 77 | 78 | Signals if the iterator has been fully opened. 79 | 80 | #### `ite.closed` 81 | 82 | Signals if the iterator has been destroyed. 83 | 84 | ## Iterator to Node.js Stream 85 | 86 | If you want to convert the iterator to a readable Node.js stream you can use the 87 | `require('nanoiterator/to-stream')` helper. 88 | 89 | ``` js 90 | var toStream = require('nanoiterator/to-stream') 91 | var stream = toStream(iterator) 92 | 93 | stream.on('data', function (data) { 94 | // calls .next() behind the scene and pushes it to the stream. 95 | }) 96 | ``` 97 | 98 | ## License 99 | 100 | MIT 101 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = NanoIterator 2 | 3 | function NanoIterator (opts) { 4 | if (!(this instanceof NanoIterator)) return new NanoIterator(opts) 5 | 6 | this.opened = false 7 | this.closed = false 8 | this.ended = false 9 | 10 | this._nextSync = false 11 | this._nextQueue = [] 12 | this._nextCallback = null 13 | this._nextDone = nextDone.bind(null, this) 14 | this._openDone = openDone.bind(null, this) 15 | 16 | if (opts) { 17 | if (opts.open) this._open = opts.open 18 | if (opts.next) this._next = opts.next 19 | if (opts.destroy) this._destroy = opts.destroy 20 | } 21 | } 22 | 23 | NanoIterator.prototype.next = function (cb) { 24 | if (this._nextCallback || this._nextQueue.length) { 25 | this._nextQueue.push(cb) 26 | return 27 | } 28 | 29 | this._nextCallback = cb 30 | this._nextSync = true 31 | if (!this.opened) this._open(this._openDone) 32 | else update(this) 33 | this._nextSync = false 34 | } 35 | 36 | NanoIterator.prototype.destroy = function (cb) { 37 | if (!cb) cb = noop 38 | 39 | if (this.closed) { 40 | this.next(() => cb()) 41 | return 42 | } 43 | 44 | this.closed = true 45 | if (!this._nextCallback) this.opened = true 46 | this.next(() => this._destroy(cb)) 47 | } 48 | 49 | NanoIterator.prototype._open = function (cb) { 50 | cb(null) 51 | } 52 | 53 | NanoIterator.prototype._destroy = function (cb) { 54 | cb(null) 55 | } 56 | 57 | NanoIterator.prototype._next = function (cb) { 58 | cb(new Error('_next is not implemented')) 59 | } 60 | 61 | if (typeof Symbol !== 'undefined' && Symbol.asyncIterator) { 62 | NanoIterator.prototype[Symbol.asyncIterator] = function () { 63 | var self = this 64 | return {next: nextPromise, return: returnPromise} 65 | 66 | function returnPromise () { 67 | return new Promise(function (resolve, reject) { 68 | self.destroy(function (err) { 69 | if (err) return reject(err) 70 | resolve({value: null, done: true}) 71 | }) 72 | }) 73 | } 74 | 75 | function nextPromise () { 76 | return new Promise(function (resolve, reject) { 77 | self.next(function (err, val) { 78 | if (err) return reject(err) 79 | resolve({value: val, done: val === null}) 80 | }) 81 | }) 82 | } 83 | } 84 | } 85 | 86 | function noop () {} 87 | 88 | function openDone (self, err) { 89 | if (err) return nextDone(self, err, null) 90 | self.opened = true 91 | update(self) 92 | } 93 | 94 | function nextDone (self, err, value) { 95 | if (self._nextSync) return nextDoneNT(self, err, value) 96 | 97 | if (self.closed) { 98 | err = new Error('Iterator is destroyed') 99 | value = null 100 | } 101 | 102 | var cb = self._nextCallback 103 | self._nextCallback = null 104 | if (!err && value === null) self.ended = true 105 | cb(err, value) 106 | 107 | if (self._nextCallback || !self._nextQueue.length) return 108 | 109 | self._nextCallback = self._nextQueue.shift() 110 | update(self) 111 | } 112 | 113 | function update (self) { 114 | if (self.ended || self.closed) nextDoneNT(self, null, null) 115 | else self._next(self._nextDone) 116 | } 117 | 118 | function nextDoneNT (self, err, val) { 119 | process.nextTick(nextDone, self, err, val) 120 | } 121 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var nanoiterator = require('./') 3 | var toStream = require('./to-stream') 4 | 5 | tape('basic', function (t) { 6 | var data = ['a', 'b', 'c', null] 7 | var expected = data.slice(0) 8 | 9 | var ite = nanoiterator({ 10 | next: cb => cb(null, data.shift()) 11 | }) 12 | 13 | ite.next(function loop (err, value) { 14 | t.error(err, 'no error') 15 | t.same(value, expected.shift(), 'expected value') 16 | if (value) ite.next(loop) 17 | else t.end() 18 | }) 19 | }) 20 | 21 | tape('concurrent next', function (t) { 22 | var data = ['a', 'b', 'c', null] 23 | 24 | var ite = nanoiterator({ 25 | next: function (cb) { 26 | t.ok(data.length > 0, 'has data') 27 | cb(null, data.shift()) 28 | } 29 | }) 30 | 31 | ite.next(function (err, value) { 32 | t.error(err, 'no error') 33 | t.same(value, 'a', 'expected a') 34 | }) 35 | ite.next(function (err, value) { 36 | t.error(err, 'no error') 37 | t.same(value, 'b', 'expected b') 38 | }) 39 | ite.next(function (err, value) { 40 | t.error(err, 'no error') 41 | t.same(value, 'c', 'expected c') 42 | }) 43 | ite.next(function (err, value) { 44 | t.error(err, 'no error') 45 | t.same(value, null, 'expected null') 46 | }) 47 | ite.next(function (err, value) { 48 | t.error(err, 'no error') 49 | t.same(value, null, 'expected null') 50 | t.end() 51 | }) 52 | }) 53 | 54 | tape('next inside next cb', function (t) { 55 | var n = 0 56 | var ite = nanoiterator({ 57 | next: cb => process.nextTick(cb, null, n++) 58 | }) 59 | 60 | ite.next(function (err, n) { 61 | t.error(err, 'no error') 62 | t.same(n, 0) 63 | ite.next(function (err, n) { 64 | t.error(err, 'no error') 65 | t.same(n, 1) 66 | }) 67 | ite.next(function (err, n) { 68 | t.error(err, 'no error') 69 | t.same(n, 2) 70 | t.end() 71 | }) 72 | }) 73 | }) 74 | 75 | tape('open', function (t) { 76 | t.plan(4 + 2 + 2) 77 | 78 | var cnt = 0 79 | var ite = nanoiterator({ 80 | open: function (cb) { 81 | t.notOk(ite.opened, '.opened set after open') 82 | t.pass('was opened') 83 | cb() 84 | }, 85 | next: function (cb) { 86 | t.ok(ite.opened, 'is opened') 87 | cb(null, cnt++) 88 | } 89 | }) 90 | 91 | ite.next(function (err, val) { 92 | t.error(err, 'no error') 93 | t.same(val, 0) 94 | ite.next(function (err, val) { 95 | t.error(err, 'no error') 96 | t.same(val, 1) 97 | }) 98 | }) 99 | }) 100 | 101 | tape('no next', function (t) { 102 | t.plan(2) 103 | 104 | var ite = nanoiterator() 105 | ite.next(function (err) { 106 | t.same(err, new Error('_next was not implemented')) 107 | }) 108 | 109 | ite = nanoiterator({}) 110 | ite.next(function (err) { 111 | t.same(err, new Error('_next was not implemented')) 112 | }) 113 | }) 114 | 115 | tape('destroy', function (t) { 116 | t.plan(3) 117 | 118 | var ite = nanoiterator({ 119 | destroy: function (cb) { 120 | t.pass('_destroy is called') 121 | cb() 122 | } 123 | }) 124 | 125 | ite.destroy(function () { 126 | t.ok(ite.closed, 'destroyed') 127 | }) 128 | ite.next(function (err) { 129 | t.same(err, new Error('Iterator is destroyed')) 130 | }) 131 | }) 132 | 133 | tape('destroy while next', function (t) { 134 | t.plan(3) 135 | 136 | var ite = nanoiterator({ 137 | next: function (cb) { 138 | t.fail('_next is never called') 139 | }, 140 | destroy: function (cb) { 141 | t.pass('_destroy is called') 142 | cb() 143 | } 144 | }) 145 | 146 | ite.destroy(function () { 147 | t.ok(ite.closed, 'destroyed') 148 | }) 149 | ite.next(function (err) { 150 | t.same(err, new Error('Iterator is destroyed')) 151 | }) 152 | }) 153 | 154 | tape('open fails', function (t) { 155 | var ite = nanoiterator({ 156 | open: function (cb) { 157 | cb(new Error('open fails')) 158 | }, 159 | next: function () { 160 | t.fail('next should not be called') 161 | } 162 | }) 163 | 164 | ite.next(function (err) { 165 | t.same(err, new Error('open fails')) 166 | t.end() 167 | }) 168 | }) 169 | 170 | tape('destroy optional callback', function (t) { 171 | t.plan(3) 172 | 173 | var ite = nanoiterator({ 174 | next: function (cb) { 175 | t.pass('_next is called') 176 | process.nextTick(cb) 177 | }, 178 | destroy: function (cb) { 179 | t.pass('_destroy is called') 180 | cb() 181 | } 182 | }) 183 | 184 | ite.next(function (err) { 185 | t.same(err, new Error('Iterator is destroyed')) 186 | }) 187 | ite.destroy() 188 | }) 189 | 190 | tape('destroy with default _destroy', function (t) { 191 | t.plan(2) 192 | 193 | var ite = nanoiterator() 194 | 195 | ite.destroy(function () { 196 | t.ok(ite.closed, 'should be closed') 197 | }) 198 | ite.next(function (err) { 199 | t.same(err, new Error('Iterator is destroyed')) 200 | }) 201 | }) 202 | 203 | tape('destroy twice', function (t) { 204 | t.plan(5) 205 | 206 | var ite = nanoiterator() 207 | var first = true 208 | 209 | ite.destroy(function () { 210 | t.ok(first, 'is first') 211 | t.ok(ite.closed, 'should be closed') 212 | first = false 213 | }) 214 | ite.destroy(function () { 215 | t.notOk(first, 'is not first') 216 | t.ok(ite.closed, 'should be closed') 217 | }) 218 | ite.next(function (err) { 219 | t.same(err, new Error('Iterator is destroyed')) 220 | }) 221 | }) 222 | 223 | tape('to-stream', function (t) { 224 | var data = ['a', 'b', 'c', null] 225 | var expected = data.slice(0, -1) 226 | 227 | var ite = nanoiterator({ 228 | next: cb => cb(null, data.shift()) 229 | }) 230 | 231 | var s = toStream(ite) 232 | 233 | s.on('data', function (data) { 234 | t.same(data, expected.shift()) 235 | }) 236 | 237 | s.on('end', function () { 238 | t.same(expected.length, 0) 239 | t.end() 240 | }) 241 | }) 242 | 243 | tape('to-stream error', function (t) { 244 | t.plan(3) 245 | 246 | var ite = nanoiterator({ 247 | next: cb => cb(new Error('stop')), 248 | destroy: function (cb) { 249 | t.pass('destroy called') 250 | cb() 251 | } 252 | }) 253 | 254 | var s = toStream(ite) 255 | 256 | s.on('error', function (err) { 257 | t.same(err, new Error('stop')) 258 | }) 259 | 260 | s.on('close', function () { 261 | t.pass('closed') 262 | }) 263 | 264 | s.resume() 265 | }) 266 | 267 | tape('to-stream destroy', function (t) { 268 | t.plan(3) 269 | 270 | var ite = nanoiterator({ 271 | next: cb => cb(null, null), 272 | destroy: function (cb) { 273 | t.pass('destroy called') 274 | cb() 275 | } 276 | }) 277 | 278 | var s = toStream(ite) 279 | 280 | s.on('error', function (err) { 281 | t.same(err, new Error('stop')) 282 | }) 283 | 284 | s.on('close', function () { 285 | t.pass('closed') 286 | }) 287 | 288 | s.destroy(new Error('stop')) 289 | }) 290 | 291 | tape('to-stream destroy errors', function (t) { 292 | t.plan(3) 293 | 294 | var ite = nanoiterator({ 295 | next: cb => cb(null, 'hi'), 296 | destroy: function (cb) { 297 | t.pass('destroy called') 298 | cb(new Error('stop')) 299 | } 300 | }) 301 | 302 | var s = toStream(ite) 303 | 304 | s.on('error', function (err) { 305 | t.same(err, new Error('stop')) 306 | }) 307 | 308 | s.on('close', function () { 309 | t.pass('closed') 310 | }) 311 | 312 | s.destroy() 313 | }) 314 | 315 | tape('to-stream destroy twice', function (t) { 316 | t.plan(3) 317 | 318 | var ite = nanoiterator({ 319 | next: cb => cb(null, 'hi'), 320 | destroy: function (cb) { 321 | t.pass('destroy called') 322 | cb(new Error('stop')) 323 | } 324 | }) 325 | 326 | var s = toStream(ite) 327 | 328 | s.on('error', function (err) { 329 | t.same(err, new Error('stop')) 330 | }) 331 | 332 | s.on('close', function () { 333 | t.pass('closed') 334 | }) 335 | 336 | s.destroy() 337 | s.destroy() 338 | }) 339 | 340 | tape('always async', function (t) { 341 | var ite = nanoiterator({ 342 | next: cb => cb(null, 'hi') 343 | }) 344 | 345 | var sync = true 346 | ite.next(function () { 347 | t.notOk(sync, 'not sync') 348 | sync = true 349 | ite.destroy(function () { 350 | t.notOk(sync, 'not sync') 351 | t.end() 352 | }) 353 | sync = false 354 | }) 355 | sync = false 356 | }) 357 | 358 | tape('not double async', function (t) { 359 | var ite = nanoiterator({ 360 | next: cb => process.nextTick(cb, null, 'hi') 361 | }) 362 | 363 | var flag = false 364 | ite.next(function () { 365 | t.notOk(flag, 'not set') 366 | t.end() 367 | }) 368 | 369 | process.nextTick(function () { 370 | flag = true 371 | }) 372 | }) 373 | --------------------------------------------------------------------------------