Skip to content

feat(rxjs): add complete pipeable API experiment - #7628

Draft
benlesh wants to merge 7 commits into
ReactiveX:masterfrom
benlesh:rxjs-9-pipeable
Draft

feat(rxjs): add complete pipeable API experiment#7628
benlesh wants to merge 7 commits into
ReactiveX:masterfrom
benlesh:rxjs-9-pipeable

Conversation

@benlesh

@benlesh benlesh commented Aug 7, 2026

Copy link
Copy Markdown
Member

Important

This is an exploratory Draft PR. It is intentionally available for review and hands-on testing, but it is not yet proposed as the final RxJS 9 release architecture. In particular, the root-import bundle cost described below must be resolved or explicitly accepted.

What this adds

This PR explores a complete functional/pipeable API for the current RxJS Next catalog while retaining the exact-Symbol API for comparison and compatibility evidence.

  • Adds rx(source, ...functions) for left-to-right composition.
  • Adds ordinary functions for all 91 source-bound capabilities and all 12 static capabilities.
  • Uses distinct *With names for the six capabilities that have both static and source-bound forms: combineWith, combineLatestWith, concatWith, mergeWith, onErrorResumeNextWith, and raceWith.
  • Keeps all four async-iteration terminals as exact AsyncGenerator results.
  • Retains all 97 public exact Symbols, with complete rxjs/symbol barrel and focused paths.
  • Adds rxjs/pipeable, rxjs/static, and focused per-capability paths.
  • Preserves existing overloads, type guards, tuple inference, conditional results, and receiver restrictions through an AST-backed generated facade and freshness check.
  • Shares reviewed implementation callbacks between pipeable and Symbol forms where implemented directly; the broader catalog currently delegates through the established exact-Symbol implementations.

The existing rxjs/map-style deep paths still expose Symbols in this experiment. Moving those established paths to functions is deliberately left as a separate review decision.

Basic usage

import { filter, map, rx } from 'rxjs';

const result = rx(
  [1, 2, 3, 4],
  filter((value) => value % 2 === 0),
  map((value, index) => `${index}: ${value * 10}`)
);

result.subscribe(console.log);
// 0: 20
// 1: 40

The functions are also available through the dedicated barrel:

import { filter, map } from 'rxjs/pipeable';
import { rx } from 'rxjs';

Static functions and *With operators

Static creation and source-bound composition use separate names:

import { merge, mergeWith, rx } from 'rxjs';

const created = merge([[1, 2], ['three']]);
const extended = rx([1, 2], mergeWith([['three']]));

Static functions are also available from rxjs/static; focused forms use paths such as rxjs/static/merge and rxjs/pipeable/merge-with.

AsyncIterable terminals

Async-iteration remains a real composition boundary. rx returns the generator directly:

import { iterateEachValue, map, rx } from 'rxjs';

const values = rx(
  [1, 2, 3],
  map((value) => value * 10),
  iterateEachValue()
);

for await (const value of values) {
  console.log(value);
}

iterateBufferedValues, iterateLatestValue, and iterateNextValue follow the same rule.

Why there is no private static-to-pipeable hook

A hook on a static function cannot make rx(source, merge(other)) work: JavaScript evaluates merge(other) before rx receives its arguments, so rx sees the returned Observable rather than the decorated function. Branding that result would add hidden context-sensitive behavior.

The TypeScript prototypes also lost the generic source/output relationship or callback contextual inference when the branded result was threaded through fixed or recursive composition types. Separate merge/mergeWith-style names preserve both runtime clarity and useful inference.

Trying the branch

git fetch https://github.com/benlesh/rxjs.git rxjs-9-pipeable
git switch --detach FETCH_HEAD
pnpm install
pnpm --filter rxjs run test:package

The most complete design and usage guide is packages/rxjs/docs/PIPEABLE_EXPERIMENT.md. The public overview is also reflected in packages/rxjs/README.md and packages/rxjs/docs/API.md.

Validation

  • 114 RxJS source files / 780 tests passed.
  • Complete rxjs build, declaration, package-import, migration-document, and generated-surface gates passed.
  • All 97 direct Symbol installation audits passed.
  • Focused complete-catalog runtime/type tests passed.
  • Lint passed with zero errors (the repository's existing warnings remain).
  • The classified compatibility audits retained their accepted baselines: 2,299/2,338 cold and 2,316/2,338 polyfill cases.

Known release blocker: root bundle retention

The complete root currently re-exports facades from modules that also install exact Symbols. Consequently, importing the experimental root retains most of the catalog and the production Webpack fixture emits 64,874 bytes against the unchanged 22,000-byte release ceiling.

That failing budget is intentionally preserved and documented; this PR does not propose increasing it. Focused imports let people test bounded graphs, but the root should not ship in this form unless shared implementations are separated from Symbol installation or the retention cost is explicitly accepted.

Review questions

  1. Should shared operator implementations be split from exact-Symbol installation before a root pipeable API can ship?
  2. Should established rxjs/map-style paths eventually switch to functions, with Symbols living under rxjs/symbol/*?
  3. Is nine precisely inferred rx transformations plus an unknown fallback the right TypeScript horizon?
  4. Should operate, the Observable-returning terminals, and the lite subscription terminal remain internal/experimental or become public architecture?

Sign up for free to 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