Skip to content

feat: stopWhen / stopWhenSync source adapters (0.9.2) - #12

Open
ildella wants to merge 2 commits into
masterfrom
stop-when
Open

feat: stopWhen / stopWhenSync source adapters (0.9.2)#12
ildella wants to merge 2 commits into
masterfrom
stop-when

Conversation

@ildella

Copy link
Copy Markdown
Owner

What

New source adapter, promoted from nucube-app where it already replaced hand-rolled stop logic in the MusicBrainz enrichment pipeline (second time the pattern was requested by a consumer):

```js
export const stopWhen = (items, predicate = () => false) => ({
async * [Symbol.asyncIterator] () {
for await (const item of items) {
if (predicate(item))
return
yield item
}
},
})
// + stopWhenSync with Symbol.iterator
```

Usage — cancellation lives in the source, everything else stays in its own place:

```js
await series(enrichOne, {
total: albums.length,
pause: ENRICH_DELAY_MS,
pauseOnErrors: true,
onProgress: onItem,
})(stopWhen(albums, shouldStop))

stopWhen(folders, () => count >= limit) // limits close over counters
```

Contract (pinned by tests)

  • Check before yield — the triggering item is pulled but never processed ("cancel before work")
  • Pull-lazy — once stopped, the underlying source is abandoned mid-stream; native cleanup (`finally`, `iterator.return()`) still runs (locked with `trackedSource`)
  • Clean completion, not an error — consumers see `failure: false`, empty `sourceErrors`; cancel is a shorter run
  • Predicate throws are source errors — `onSourceError` / `sourceErrors`, never `onError`
  • Predicate is sync, receives `(item, index)`, defaults to `() => false` (identity wrapper)
  • Length forwarding — array sources forward `.length` on the wrapper so `series` keeps progress totals without an explicit `total`; generators keep total omitted
  • Composes with `series` / `scan` / `reduce` / `filter` / `findSync` and raw `for await` with zero changes to those functions

Design notes for reviewers

  • Source wrapper, not a `series` option. An option would only cover `series`/`filter`; it would never reach `reduce(ingestEvents())` or raw `for await` loops. The wrapper helps every consumer at once. A native `while:` option was considered and rejected; if we ever want one, `stopWhen` still stands alone.
  • Not intra-item abort. Work already started on an item is not interrupted — that stays in the operation (AbortController). Documented in reference/patterns.
  • Bug fix included: `filter` / `filterSync` / `findSync` sniff their first argument and treated any non-array object as a `where()` pattern — so `filter(pages(), pred)` silently returned a curried function instead of running. `isPatternObject` (in `shared.js`) now excludes objects implementing an iteration protocol. Patterns (`{active: true}`) are unaffected; regression tests cover generators directly.

Out of scope (explicitly)

  • native `while`/`until` option on consumers · async predicates · currying · `takeWhile` alias · changes to `take`

Testing

34 new tests (`tests/stop-when.test.js`, `tests/stop-when-sync.test.js`): nucube's 7 ported verbatim in intent + composition (series/reduce/filter/take), length forwarding, index passing, closure predicates, predicate-throws-as-source-error, generator cleanup. Full suite: 400 passed. Lint clean.

Docs

reference.md (new entry + TOC), guide.md, patterns.md (Pattern 9), examples.md (MusicBrainz-shaped example), architecture.md (one sentence under Eager Execution), migration.md (0.9.2 section incl. the filter fix note), skills/core/SKILL.md, README. Version bumped to 0.9.2.

Follow-up (not here)

nucube-app can delete `src/lib/iterables/stop-when.js` and import from `pipelean@0.9.2` with no call-site change; it may then drop its explicit `total: albums.length`.

stopWhen(items, predicate) stops pulling from any iterable once the
predicate is truthy, checked before the item is offered downstream.
Promoted from nucube-app where it replaced hand-rolled stop logic in
the MusicBrainz enrichment pipeline.
- check before yield: the triggering item is never processed
- pull-lazy: abandoned sources are left mid-stream, cleanup still runs
- clean completion: failure: false, empty sourceErrors — cancel is a
shorter run, not a source death; predicate throws are source errors
- predicate receives (item, index), sync only, defaults to () => false
- forwards numeric length from array sources so series keeps progress
totals without an explicit total option
- composes with series/scan/reduce/filter/findSync and raw for await
with zero changes to those functions
Also fixes filter/filterSync/findSync arg sniffing: iterable objects
(generators, custom adapters) were misread as where() patterns because
any non-array object matched. Patterns are now objects that do not
implement an iteration protocol.
The default `() => false` is a pass-through, not an identity wrapper.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@ildella