// ───────────────────────────────────────────────────────────────────────────── // ITERATOR [Behavioral] // ───────────────────────────────────────────────────────────────────────────── // INTENT : Provide a way to access the elements of an aggregate object sequentially without exposing its // underlying representation. // PROBLEM : Callers want `for..of` over your collection (paginated API, tree, linked list, ring buffer) // without knowing whether it's an array, a Map, pages from a server, or computed lazily. // SAY : Participants: Iterator (next() → { value, done }), ConcreteIterator (holds cursor state), // Aggregate (createIterator() — in JS: [Symbol.iterator]()), ConcreteAggregate. Pitch: "one // protocol — next()/done — so any collection works with for..of, spread and destructuring." // SEEN IN : `for..of`, spread `[...x]`, destructuring, `Array.from`, `Promise.all` — all drive // `Symbol.iterator`; Map/Set/String iterators; generators `function*`; `for await` over Node // streams, `readline`, `fs.opendir`; DB cursors (Mongo `cursor.next()`, pg-cursor); paginated SDK // iterators (AWS `paginate*`, Octokit `paginate.iterator`); Node 22+ iterator helpers (`.map/.filter/.take`). // VS : Visitor pushes an operation INTO the structure; Iterator pulls elements OUT one at a time. // Composite is the tree; Iterator is how you walk it. Observer PUSHES items to you; Iterator you // PULL — async iterators blur the line. // ───────────────────────────────────────────────────────────────────────────── 'use strict'; const assert = require('node:assert/strict'); // ── Classic shape (classes, so the participant names are visible) class PagedOrders { // ConcreteAggregate — representation: array of pages constructor(pages) { this.pages = pages; } createIterator() { return new PagedOrdersIterator(this.pages); } // Aggregate.createIterator() [Symbol.iterator]() { return this.createIterator(); } // the JS protocol hook } class PagedOrdersIterator { // ConcreteIterator — cursor state hidden from client #pages; #page = 0; #idx = 0; #fetches = 0; constructor(pages) { this.#pages = pages; } get fetches() { return this.#fetches; } next() { while (this.#page < this.#pages.length) { const page = this.#pages[this.#page]; if (this.#idx === 0) this.#fetches += 1; // one "network call" per page if (this.#idx < page.length) return { value: page[this.#idx++], done: false }; this.#page += 1; this.#idx = 0; } return { value: undefined, done: true }; } [Symbol.iterator]() { return this; } // iterators are themselves iterable } const orders = new PagedOrders([[{ id: 1 }, { id: 2 }], [], [{ id: 3 }]]); const it = orders.createIterator(); assert.deepEqual(it.next(), { value: { id: 1 }, done: false }); assert.deepEqual(it.next(), { value: { id: 2 }, done: false }); assert.deepEqual(it.next(), { value: { id: 3 }, done: false }); // empty page skipped transparently assert.deepEqual(it.next(), { value: undefined, done: true }); assert.deepEqual(it.next(), { value: undefined, done: true }); // stays done assert.equal(it.fetches, 3); assert.deepEqual([...orders].map((o) => o.id), [1, 2, 3]); // spread uses Symbol.iterator const [first, ...rest] = orders; // destructuring too assert.equal(first.id, 1); assert.equal(rest.length, 2); const it2 = orders[Symbol.iterator](); it2.next(); assert.equal(it2.fetches, 1); // lazy: only page 1 touched so far // ── Idiomatic JS shape: generators — cursor state lives in the suspended function frame class OrderHistory { #byCustomer = new Map(); add(customer, order) { if (!this.#byCustomer.has(customer)) this.#byCustomer.set(customer, []); this.#byCustomer.get(customer).push(order); return this; } *[Symbol.iterator]() { // default traversal: all orders, newest first per customer for (const list of this.#byCustomer.values()) for (let i = list.length - 1; i >= 0; i--) yield list[i]; } *byCustomer(name) { yield* this.#byCustomer.get(name) ?? []; } // alternative traversal, same protocol *totals() { for (const o of this) yield o.total; } } const hist = new OrderHistory() .add('ada', { id: 'a1', total: 10 }).add('ada', { id: 'a2', total: 20 }).add('bob', { id: 'b1', total: 5 }); assert.deepEqual([...hist].map((o) => o.id), ['a2', 'a1', 'b1']); assert.deepEqual([...hist.byCustomer('ada')].map((o) => o.id), ['a1', 'a2']); assert.equal([...hist.totals()].reduce((a, b) => a + b, 0), 35); assert.deepEqual(Array.from(hist.byCustomer('nobody')), []); // Lazy / infinite sequences — impossible with arrays, trivial with generators function* ids(prefix) { let n = 0; while (true) yield `${prefix}-${++n}`; } const take = (iter, n) => { const out = []; for (const v of iter) { out.push(v); if (out.length === n) break; } return out; }; assert.deepEqual(take(ids('ord'), 3), ['ord-1', 'ord-2', 'ord-3']); const gen = ids('inv'); assert.equal(gen.next().value, 'inv-1'); assert.equal(gen.next().value, 'inv-2'); // resumable cursor // Async iterator — pages from a (fake) API consumed with for await (Node streams work exactly like this) async function* fetchAllPages(api) { let cursor = null; do { const page = await api(cursor); yield* page.items; cursor = page.next; } while (cursor); } const fakeApi = async (cursor) => (cursor === null ? { items: [1, 2], next: 'p2' } : { items: [3], next: null }); (async () => { const seen = []; for await (const item of fetchAllPages(fakeApi)) seen.push(item); assert.deepEqual(seen, [1, 2, 3]); console.log('ok: design-patterns/behavioral/iterator.js'); })();