diff --git a/error.ts b/error.ts index 9997c7d..ada4493 100644 --- a/error.ts +++ b/error.ts @@ -1,16 +1,15 @@ // @filename: error.ts /** - * Error primitives and guards for Observable pipelines. + * `ObservableError` carries failures through a pipeline without losing the work + * that already happened before the failure. * - * This entrypoint explains the error values that travel through this library - * when an operator uses pass-through error handling. It exports the - * `ObservableError` class plus helper functions for narrowing and asserting - * those wrapped failures without losing the original error object, stack, or - * operator context. + * That matters because a stream can emit several useful values before one later + * stage throws. If every error immediately terminated the stream, downstream + * code could lose buffered values or context that would have helped recovery. * - * Reach for this module when you want to inspect failures as data, recover from - * an upstream step without throwing away buffered values, or surface richer - * debugging information than a plain `Error` can carry on its own. + * The helpers in this file keep the original error, stack, operator name, and + * source value attached so recovery code can make a deliberate decision instead + * of handling a generic `Error` with missing context. * * @module */ diff --git a/helpers/mod.ts b/helpers/mod.ts index 6b1030f..42c8152 100644 --- a/helpers/mod.ts +++ b/helpers/mod.ts @@ -1,24 +1,20 @@ // @filename: helpers/mod.ts /** - * High-level operator entrypoint for composing Observable pipelines. - * - * This is the ergonomic "import most things from one place" surface for the - * package. It re-exports the pipe/compose helpers, operator builders, utility - * helpers, and the built-in operator families so callers can build an entire - * pipeline without remembering which category module each operator lives in. - * - * The exports here fall into a few broad groups: - * - core transformation operators such as `map`, `filter`, `take`, and `scan` - * - timing operators such as `debounce`, `delay`, `throttle`, and `timeout` - * - combination operators such as `mergeMap`, `concatMap`, and `switchMap` - * - batching and error operators for collecting values or recovering from - * failures in a stream - * - * Everything shares the same Web Streams-based runtime, so backpressure, - * teardown, and error-mode behavior stay consistent from one operator to the - * next. Import from `./operations/*` only when you want a narrower entrypoint - * for discovery or tree-shaken docs. Import from this module when you want the - * familiar "just give me the operator toolbox" experience. + * Most pipelines only need one import path for operators, pipe helpers, and + * interop utilities. + * + * The exports here cover the common jobs you combine into a pipeline: + * + * - reshape values with `map`, `filter`, `scan`, `take`, and friends + * - coordinate time with `debounce`, `delay`, `throttle`, and `timeout` + * - start follow-up work with `mergeMap`, `concatMap`, and `switchMap` + * - recover from wrapped failures with `catchErrors`, `ignoreErrors`, and + * related helpers + * + * All of those stages share the same Web Streams-based runtime, so teardown, + * backpressure, and error-mode behavior stay consistent from one stage to the + * next. Narrower `./operations/*` paths are available when a focused category + * is easier to explore. * * ## Basic Usage * diff --git a/helpers/operations/batch.ts b/helpers/operations/batch.ts index 3c444cb..b58a770 100644 --- a/helpers/operations/batch.ts +++ b/helpers/operations/batch.ts @@ -1,14 +1,18 @@ /** - * Operators that collect multiple source values into grouped results. + * Batching operators wait for several source values before emitting one result. * - * This entrypoint contains the batching side of the operator library. Use it - * when one output value should summarize several input values, such as turning - * a whole stream into a single array with `toArray()` or bundling incoming - * items into fixed-size chunks with `batch()`. + * Use them when one output should summarize a group, for example collecting a + * whole stream into one array with `toArray()` or bundling values into fixed + * chunks with `batch()`. * - * These operators trade immediacy for aggregation. They usually hold on to some - * values until a batch fills or the source completes, so they are best for - * finite streams or bounded buffers where that extra memory is intentional. + * The trade-off is simple: less immediacy, more aggregation. + * + * ```text + * source values -> hold some values -> emit grouped result + * ``` + * + * That extra memory is usually a good trade for finite streams or deliberate + * bounded buffering. * * @module */ diff --git a/helpers/operations/combination.ts b/helpers/operations/combination.ts index 69236b3..80dce67 100644 --- a/helpers/operations/combination.ts +++ b/helpers/operations/combination.ts @@ -1,16 +1,18 @@ /** - * Operators that turn each source value into another stream and combine the - * results. + * Combination operators start follow-up streams from each source value. * - * This entrypoint covers the "flattening" family of operators such as - * `mergeMap`, `concatMap`, and `switchMap`. Use it when one input value needs - * to start follow-up async work, for example fetching related records, reading - * files, or switching to the latest search request. + * They are useful when one value should trigger more async work, such as a + * search term starting a fetch or a file path starting a file read. The outer + * stream produces the trigger value. The inner stream does the follow-up work. * - * The operators in this module mainly differ in concurrency and cancellation - * behavior. `mergeMap` keeps multiple inner streams alive at once, `concatMap` - * preserves order by running one at a time, and `switchMap` cancels older work - * when a newer source value arrives. + * The main question is what to do when a new outer value arrives before the old + * inner work has finished: + * + * ```text + * mergeMap -> keep many inner streams running at once + * concatMap -> queue inner streams and run them one at a time + * switchMap -> cancel older inner work and keep only the latest + * ``` * * @module */ @@ -42,7 +44,7 @@ import { * outer value ---> createFollowUp(value, index) ---> inner Observable * ``` * - * The operators in this module differ in what they do after that point: + * From there, each operator makes a different concurrency decision: * * ```text * mergeMap -> keep many inners running at once diff --git a/helpers/operations/conditional.ts b/helpers/operations/conditional.ts index 2810e71..0b874f6 100644 --- a/helpers/operations/conditional.ts +++ b/helpers/operations/conditional.ts @@ -1,14 +1,13 @@ /** - * Predicate and decision-oriented operators for Observable streams. + * Predicate operators answer questions about a stream. * - * This entrypoint exports the operators that answer questions about a stream or - * gate values based on a condition. These are the Observable equivalents of - * array helpers such as `every()`, `some()`, and `find()`, plus utilities that - * stop early once a decision has been reached. + * Use them when the important result is a decision rather than a transformed + * value: does every item match, does any item match, where is the first match, + * or when should processing stop because the answer is already known? * - * Reach for this module when you care about whether a stream contains a match, - * whether every value passes a rule, or when processing should stop as soon as - * the answer is known. + * These operators are the stream versions of `every()`, `some()`, `find()`, + * and related array helpers, with the added benefit that they can stop early + * instead of waiting for the whole source to finish. * * @module */ diff --git a/helpers/operations/core.ts b/helpers/operations/core.ts index 68b0ad5..72dd905 100644 --- a/helpers/operations/core.ts +++ b/helpers/operations/core.ts @@ -4,17 +4,16 @@ import type { ObservableError } from "../../error.ts"; import { createOperator, createStatefulOperator } from "../operators.ts"; /** - * Core transformation and terminal operators for everyday stream work. + * Core operators cover the array-like side of stream processing. * - * This module is the closest match to familiar array helpers. It exports the - * operators you reach for first when you want to transform values, filter them, - * accumulate state, or stop after a condition has been met. In practice, this - * is where most pipelines start before you add timing or concurrency behavior. + * Start here when the job is "change each value", "keep only some values", or + * "build a running result". If you already know `Array.map()`, + * `Array.filter()`, and `Array.reduce()`, you already know the shape of the + * work. The difference is that values arrive over time instead of all at once. * - * The important difference from arrays is error handling. These operators are - * designed to work with the library's pass-through model, so your callbacks see - * clean data values while `ObservableError` instances continue downstream - * unchanged until a dedicated error-handling step decides what to do with them. + * Wrapped failures follow a separate path. In pass-through mode, ordinary data + * callbacks keep seeing ordinary values while `ObservableError` instances move + * downstream until an error-focused operator decides what to do with them. * * @module */ diff --git a/helpers/operations/errors.ts b/helpers/operations/errors.ts index e5b8bf7..f831cde 100644 --- a/helpers/operations/errors.ts +++ b/helpers/operations/errors.ts @@ -1,14 +1,17 @@ /** - * Error-focused operators for recovering from or reshaping stream failures. + * Error operators decide what should happen after a stage fails. * - * This entrypoint is for pipelines that expect some work to fail and want to - * keep going. It exports helpers for dropping wrapped errors, mapping them to - * fallback values, logging them, or converting them into a shape that fits the - * rest of the pipeline. + * In pass-through mode, a thrown error does not have to end the whole pipeline + * immediately. It can travel downstream as an `ObservableError` value instead. + * These operators are the place where that wrapped failure becomes a real + * policy decision: drop it, replace it, log it, summarize it, or throw it. * - * These operators are most useful with the library's pass-through error mode, - * where failures travel as `ObservableError` values instead of immediately - * terminating the whole stream. + * ```text + * source value -> stage throws -> ObservableError -> error operator -> next step + * ``` + * + * That separation lets data operators stay simple while recovery logic stays + * explicit. * * @module */ diff --git a/helpers/operations/mod.ts b/helpers/operations/mod.ts index 232eb2d..b01f72c 100644 --- a/helpers/operations/mod.ts +++ b/helpers/operations/mod.ts @@ -1,18 +1,15 @@ /** - * Category-level entrypoint for the built-in Observable operations. + * Built-in operators fall into a few broad jobs, and these re-exports keep + * those groups together. * - * This module gathers every operator category that powers the higher-level - * `./operators` entrypoint. It is useful when you want a focused import path - * for documentation and discovery, but still want access to the full built-in - * operator set from one module. + * - `./core` handles array-like transforms such as `map`, `filter`, and `scan` + * - `./timing` handles spacing and deadlines such as `debounce` and `timeout` + * - `./combination` handles follow-up streams such as `mergeMap` and `switchMap` + * - `./batch`, `./conditional`, and `./errors` handle collection, decisions, + * and recovery * - * The re-exports are grouped by job: - * - `./core` covers the array-like transformations and terminal operators - * - `./timing` covers time-based coordination such as debounce and timeout - * - `./combination` covers flattening and concurrency helpers such as - * `mergeMap`, `concatMap`, and `switchMap` - * - `./batch`, `./conditional`, and `./errors` cover collection, predicate, and - * recovery-focused utilities + * Import from these grouped paths when the job matters more than the exact file + * name. * * @module */ diff --git a/helpers/operations/timing.ts b/helpers/operations/timing.ts index 60ebd20..99205f4 100644 --- a/helpers/operations/timing.ts +++ b/helpers/operations/timing.ts @@ -1,14 +1,19 @@ /** - * Time-based operators for spacing, delaying, and expiring stream values. + * Timing operators control when values are allowed to move downstream. * - * This entrypoint groups the operators that make time part of your pipeline's - * behavior. Use it for UI patterns such as debouncing search input, throttling - * bursty events, delaying retries, or timing out work that takes too long. + * Use them for UI and network patterns where time is the real problem: wait for + * typing to stop, limit bursty input, delay retries, or fail work that takes + * too long. * - * Timing operators do not just change values; they change when work is allowed - * to move downstream. That makes them especially important for coordinating - * async side effects without piling up stale requests or overwhelming slower - * consumers. + * They do not mainly change the value. They change the schedule around the + * value. + * + * ```text + * source event -> wait, delay, throttle, or expire -> next stage + * ``` + * + * That makes them useful for preventing stale requests, noisy event bursts, and + * accidental overload on slower consumers. * * @module */ diff --git a/helpers/operators.ts b/helpers/operators.ts index 2cdea68..a6d5810 100644 --- a/helpers/operators.ts +++ b/helpers/operators.ts @@ -1,221 +1,103 @@ /** - * Operators are the building blocks of Observable pipelines. + * Operators reshape a stream without consuming it. * - * If you've ever used `Array.map` or `Array.filter`, you already know the core idea: - * an **operator** takes a sequence of values and transforms, filters, or combines them - * into a new sequence. Operators let you build data pipelines, think of them as the - * Lego bricks for working with streams of data. + * If `subscribe()` answers "what should happen when a value arrives?", an + * operator answers "what should the next value look like, and when should it + * arrive?" Operators let one Observable feed another, so a button click can + * become a debounced search term, a search term can become a network request, + * and a network request can become parsed JSON. * - * Think of an operator as a function that takes a stream of values and returns a new stream, - * transforming, filtering, or combining the data as it flows through. + * Start with the array analogy, then extend it. `Array.map()` and + * `Array.filter()` work on values you already have in memory. Observable + * operators do similar jobs for values that arrive over time. * - * For example, to double every number in an array: - * ```ts - * [1, 2, 3].map(x => x * 2); // [2, 4, 6] + * ```text + * array values -> map/filter -> final array + * stream values -> operators -> next Observable * ``` * - * With Observables, you want to do the same thing, but for values that arrive over time: - * ```ts - * // Double every number in a stream - * const double = createOperator({ - * transform(chunk, controller) { - * controller.enqueue(chunk * 2); - * } - * }); + * That time dimension changes what the operator runtime has to manage: + * cancellation, backpressure, teardown, and failures that might happen after + * some values have already flowed downstream. * - * // Only allow even numbers through - * const evens = createOperator({ - * transform(chunk, controller) { - * if (chunk % 2 === 0) controller.enqueue(chunk); - * } - * }); + * These operator builders lean on `TransformStream` because it already models + * "read one chunk, write zero or more chunks" well. The builders add the rules + * that matter for Observable pipelines: * - * // Use them together in a pipeline - * pipe( - * Observable.from([1, 2, 3, 4]), - * double, - * evens - * ).subscribe(console.log); // Output: 4, 8 - * ``` - * - * This module lets you build your own operators using the Web Streams API under the hood. - * Why streams? Because they're fast, memory-efficient, and let you process data as it arrives, - * not just after everything is loaded. This is especially useful for things like file processing, - * network requests, or any situation where you want to handle data piece-by-piece. + * - wrapped errors can keep moving downstream instead of killing the whole + * pipeline immediately + * - cancellation from later stages tears earlier work down predictably + * - stateful stages create fresh state per subscription instead of sharing it + * accidentally * - * ## Why Streams? Why Not Just Arrays? + * The default error mode is the biggest mental shift. In pass-through mode, a + * thrown error becomes an `ObservableError` value. Later stages can recover, + * drop, summarize, or rethrow it. * - * Arrays are great for data you already have. But what about data that arrives slowly, - * or is too big to fit in memory? Think files, network responses, or user events. - * That's where **streams** shine: they let you process data piece-by-piece, as it arrives, - * without waiting for everything or loading it all at once. - * - * The Web Streams API (and Node.js streams) are the standard way to do this in modern JavaScript. - * But using them directly is verbose and error-prone: - * ```ts - * // Native TransformStream: double every number - * const stream = new TransformStream({ - * transform(chunk, controller) { - * controller.enqueue(chunk * 2); - * } - * }); - * ``` - * Your operator helpers let you write the same thing, but with less boilerplate and - * built-in error handling: - * ```ts - * const double = createOperator({ - * transform(chunk, controller) { - * controller.enqueue(chunk * 2); - * } - * }); + * ```text + * clean value -> transform callback runs -> transformed value + * thrown error -> wrapped as ObservableError -> downstream error stage decides * ``` * - * By building operators on top of streams, you get: - * - **Backpressure**: Slow consumers don't overwhelm fast producers. - * - **Low memory usage**: Process data chunk-by-chunk, not all at once. - * - **Composable pipelines**: Easily chain transformations. + * That is why built-in data operators such as `map()` and `filter()` keep your + * callback focused on ordinary values. Wrapped errors bypass the callback and + * keep moving until an error-oriented operator handles them. * - * ## Connecting Operators: Pipelines - * - * Operators are most powerful when you chain them together. This is called a pipeline. - * - * It's just like chaining `map` and `filter` on arrays, but for streams: - * ```ts - * pipe( - * Observable.from([1, 2, 3, 4]), - * createOperator({ - * transform(chunk, controller) { - * controller.enqueue(chunk * 2); - * } - * }), - * createOperator({ - * transform(chunk, controller) { - * if (chunk % 3 === 0) controller.enqueue(chunk); - * } - * }) - * ).subscribe(console.log); // Output: 6 - * ``` + * `createStatefulOperator()` adds one more piece: memory that belongs to one + * subscription. That is useful for running totals, moving windows, and other + * logic where later chunks depend on earlier ones. * - * Compare to arrays: + * @example Building a simple custom operator * ```ts - * [1, 2, 3, 4] - * .map(x => x * 2) - * .filter(x => x % 3 === 0) - * .forEach(console.log); // [2, 4, 8] - * ``` - * - * Of course, no one wants to write operators from scratch every time. - * So we provide some core operations via basic familiar operators, - * plus error handling utilities to make your pipelines robust. + * const double = createOperator({ + * name: 'double', + * transform(value, controller) { + * controller.enqueue(value * 2); + * }, + * }); * - * Aka, `map`, `filter`, `reduce`, `batch`, `catchErrors`, `ignoreErrors`, and more. - * So really the example above becomes: - * ```ts * pipe( - * Observable.from([1, 2, 3, 4]), - * map(x => x * 2), - * filter(x => x % 3 === 0) - * ).subscribe(console.log); // Output: 2, 4, 8 + * Observable.from([1, 2, 3]), + * double, + * ).subscribe(console.log); + * // 2, 4, 6 * ``` * - * The example is not ideal given arrays have functions for this already, - * but you get the idea. It's meant more for streams of data that arrive over time. - * - * ## Error Handling: Real-World Data is Messy - * - * Real-world data is messy. Sometimes things go wrong aka, maybe a chunk is malformed, or a network - * request fails. Our operators let you choose how to handle errors, with four modes: - * - * - `"pass-through"` (default): Errors become special values in the stream, so you can handle them downstream. Imagine almost like bubble wrap over error since they are dangerous allowing us to make sure we don't break the flow. - * - `"ignore"`: Errors are silently skipped. The stream keeps going as if nothing happened. Imagine that we're basically just remove any errors from the stream while it's flowing (pretty stressful ngl). - * - `"throw"`: The stream stops immediately on the first error. Basically start screaming bloody murder, an error has occured so everything must stop. - * - `"manual"`: You handle all errors yourself. If you don't catch them, the stream will error. This is primarily for operators who have special error handling requirements. - * - * Example: parsing JSON safely + * @example Recovering from bad JSON without stopping the stream * ```ts - * // Pass-through: errors become ObservableError values ( - * // we basically package errors in bubble wrap which we call a ObservableError - * const safeParse = createOperator({ - * errorMode: "pass-through", - * transform(chunk, controller) { - * controller.enqueue(JSON.parse(chunk)); - * } - * }); - * - * // Ignore: errors are dropped - * const ignoreParse = createOperator({ - * errorMode: "ignore", - * transform(chunk, controller) { - * controller.enqueue(JSON.parse(chunk)); - * } - * }); - * - * // Throw: stream stops on first error - * const strictParse = createOperator({ - * errorMode: "throw", - * transform(chunk, controller) { - * controller.enqueue(JSON.parse(chunk)); - * } + * const parseJson = createOperator({ + * name: 'parseJson', + * errorMode: 'pass-through', + * transform(value, controller) { + * controller.enqueue(JSON.parse(value)); + * }, * }); - * ``` * - * Compare to native TransformStream error handling: - * ```ts - * // Native: you must handle errors yourself - * const stream = new TransformStream({ - * transform(chunk, controller) { - * try { - * controller.enqueue(JSON.parse(chunk)); - * } catch (err) { - * controller.error(err); // This kills the stream - * } - * } - * }); + * pipe( + * Observable.from(['{"ok":true}', 'bad json', '{"ok":false}']), + * parseJson, + * catchErrors({ ok: null }), + * ).subscribe(console.log); * ``` * - * ## Stateful Operators: Remembering Across Chunks - * - * Sometimes you need to keep track of things as data flows through, like running totals, - * buffers, or windows. Your `createStatefulOperator` lets you do this easily: - * + * @example Keeping state per subscription * ```ts - * // Running sum - * const runningSum = createStatefulOperator({ + * const runningSum = createStatefulOperator({ + * name: 'runningSum', * createState: () => ({ sum: 0 }), - * transform(chunk, state, controller) { - * state.sum += chunk; + * transform(value, state, controller) { + * state.sum += value; * controller.enqueue(state.sum); - * } + * }, * }); * * pipe( * Observable.from([1, 2, 3]), - * runningSum - * ).subscribe(console.log); // Output: 1, 3, 6 + * runningSum, + * ).subscribe(console.log); + * // 1, 3, 6 * ``` * - * Native TransformStream can't do this as cleanly, you'd have to manage state outside the stream, - * which gets messy, error-prone and annoying real quick. - * - * ## Performance and Memory - * - * - **Hot path optimization**: The error handling logic is generated for each operator, - * so there are no runtime branches inside your data processing loop. - * - **Memory safety**: Only the functions and state you need are kept alive; everything else - * can be garbage collected. - * - **Streams scale**: You can process gigabytes of data with minimal RAM, and your operators - * work just as well for infinite streams as for arrays (though arrays have better performance through - * their built-in `filter`, `map`, `forEach`, etc..., methods). - * - * ## Summary - * - * - Operators are like `Array.map`/`filter`, but for async streams of data. - * - You can build pipelines that transform, filter, buffer, or combine data. - * - Error handling is flexible and explicit. - * - Streams make your code scalable and memory-efficient. - * - State is easy to manage for advanced use cases. - * - The helpers make working with streams as easy as working with arrays. - * * @module */