# RxJS Next architecture ## Executive summary RxJS Next is changing RxJS from an owner of the Observable primitive into an extension library for the web-platform Observable. The target architecture has three conceptual layers: 1. **Platform acquisition:** use a native `Observable`, or install a conforming fallback when it is absent. 2. **RxJS extensions:** patch exported Symbol-keyed factories and operators onto the selected constructor or prototype. 3. **Migration tooling:** provide one canonical portable Skill, thin harness adapters, and a bounded deterministic transform engine based on stabilized runtime contracts and classified RxJS 7 behavioral evidence. The branch now has a buildable three-package foundation for the platform and extension layers, but it remains a prototype rather than a release. The fallback is held to every selected Observable test at the pinned WPT revision; there are no RxJS-specific conformance exceptions. P0.3 implements the package, installation, detection, and initial realm boundaries accepted in D-039 through D-041. P0.4 adds the shared lifecycle safety rail. P0.5 pins the written Observable rules and executable WPT gate and restores complete conformance by superseding D-042 with D-045. ## Architecture context The branch was created by replacing most of the `packages/rxjs` implementation and its RxJS 7 tests with a small platform-based experiment. The initiating commit describes it as “a new implementation built on top of the platform observable (using the polyfill for now).” The rest of the monorepo remains largely RxJS 7-era infrastructure: - the root README and documentation application describe the existing generation; - package manifests use the first RxJS 9 prerelease version, `9.0.0-beta.0`; - the inherited `@rxjs/observable` workspace package has been removed; - release and CI paths are being redesigned for the accepted RxJS 9 support matrix; package documentation is local, while the documentation application remains outside this workstream. Those artifacts are useful history and migration evidence, but they are not automatically part of the target architecture. ## Target system context ```mermaid flowchart LR Runtime["Runtime realm"] --> Native{"Observable exists?"} Native -->|Yes| Active["Active Observable constructor"] Native -->|No| Polyfill["Conforming fallback"] Polyfill --> Active Extensions["RxJS Symbol extension modules"] -->|patch exported Symbol keys| Active App["Application or library"] -->|imports Symbols and entry points| Extensions App -->|constructs, subscribes, and composes| Active Tooling["Canonical migration Skill and deterministic engine"] -.-> App Legacy["Migrating RxJS 7 application"] -->|adopts explicit Next APIs| App Tooling -.-> Legacy ``` ## Single-maintainer release boundary RxJS 9 explicitly assumes one human author, reviewer, merger, release operator, and security responder. Pull requests expose changes and run required checks; they are not evidence of independent approval. Beta publication is a local, interactive operation from a clean `master` checkout that exactly matches its remote. `pnpm release:beta ` validates the synchronized four-package version, runs repository and package gates, packs the packages, prints their SHA-512 integrities, and runs npm publication dry runs. Ben must then type the exact version before npm's own OTP/WebAuthn flow publishes each tarball under `next`. The supporting packages publish first and `rxjs` publishes last. Registry integrity and dist-tags are verified before the command reports success. CI has no npm publishing credential and no workflow can publish. The design deliberately trusts Ben's local machine and npm account at the publication boundary instead of adding a GitHub App, trusted publisher, private staging, release environment, or automated release-PR system. This keeps the process understandable and makes the residual risk explicit: a compromised maintainer machine or npm authentication can still compromise a release. Required CI, interactive WebAuthn, exact package ordering, dry runs, and registry-integrity verification reduce mistakes without pretending to remove that trust. Useful producer-per-subscription values and Subjects remain intentional APIs inside `rxjs`; they do not form a separate compatibility layer or package. Migration tooling is not a runtime dependency. ## Current component inventory | Component | Current responsibility | Intended responsibility | Current gap | | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | `packages/observable-polyfill` | Conditionally supplies the ambient platform-shaped `Observable`, paired `Subscriber`, native-style methods, `EventTarget.when()`, and fallback metadata | Independently publishable conditional fallback and owner of the base ambient platform types | P6.2 must complete the accepted runtime matrix and release gate | | `packages/rxjs` | Installs entry-scoped Symbol operators, factories, and async-iteration adapters by direct exact-Symbol assignment; exports intentional subjects, producer-per-subscription primitives, notifications, and errors | Main Symbol-extension library with direct exact-Symbol assignment plus intentional non-operator RxJS Next APIs | P6.2 must complete bundle-budget and release qualification | | `packages/rxjs/src/testing` | Contains obsolete exploratory fake timers and an experimental `ScheduledObservable` | Retained only as prototype history until removed | Superseded by the accepted `@rxjs/test` boundary | | `packages/test` | Provides `rxTest`, marble factories/assertions, virtual host scheduling, and explicit cold/hot/platform source models | Implementation-neutral framework testing that consumes an already active realm Observable | P6.2 must complete the accepted runtime matrix | | `packages/migrate` | Provides a versioned deterministic engine, canonical portable Skill, safe Skill installer, structured CLIs, capability and contract schemas, package/fixture gates, and committed Codex qualification records | Deterministic migration engine and canonical versioned Skill; never a runtime dependency | Broader repository, capability, model, and non-Codex outcome qualification remains future work | | `apps/rxjs.dev` | Existing RxJS documentation site | Maintained independently and integrated only in a later explicitly coordinated change | Represents the prior generation and is outside this project plan's edit, build, and publish scope | ## Platform Observable lifecycle ### Intended semantics The living Observable specification associates each Observable with a weak reference to an active `Subscriber`. A first observer starts producer work. Additional observers join that active subscriber. Aborting an observer removes it; when the last observer leaves, the subscriber closes and producer teardown runs. A later observer can start a new producer subscription. Do not summarize that lifecycle by assigning one persistent “hot” or “cold” label to the Observable. Those terms describe when the producer exists relative to a particular subscription: - **Cold:** subscription creates the producer. - **Hot:** the producer already exists before subscription. The first platform subscription creates the active producer. A concurrent subscription joins that already-existing producer. After the ref count reaches zero, a later subscription creates a new producer. Sharing, multicasting, replay, and ref counting are separate properties and do not redefine those terms. In particular, an instantiated `Subject` is hot because the producer exists before observers subscribe. See D-035. ```mermaid stateDiagram-v2 [*] --> Idle Idle --> Active: first observer subscribes Active --> Active: more observers join Active --> Active: one observer aborts
others remain Active --> Idle: last observer aborts
teardown runs Active --> Idle: producer completes or errors Idle --> Active: later observer subscribes
new producer run ``` Each active `Subscriber`: - owns the internal observer list; - exposes `next`, `error`, and `complete`; - exposes an `AbortSignal`; - accepts teardown callbacks; - ignores `next` and `complete` after closure and reports invalid late errors according to the platform algorithm. This lifecycle is a core architectural constraint, not an implementation detail. Operators on the platform surface must not create independent upstream producer work for each observer. An intentional type such as `ColdObservable` may expose a different direct-subscription contract without redefining the platform surface. ### Current implementation `packages/observable-polyfill/src/index.ts` models the shared lifecycle with: - a `WeakRef>` on each `ObservableImpl`; - a `Set` of safe observers on the active subscriber; - an internal `AbortController`; - ref-count closure when the observer set becomes empty; - explicit close state that aborts the subscriber signal before running teardown callbacks in reverse insertion order; - a required `next(value)` argument for every platform `Subscriber`, including `Subscriber`; explicit `next(undefined)` is valid for a void subscriber, while an omitted runtime argument throws before active-state or notification-delivery checks; - immediate execution of teardowns registered after closure; - a small `AbortController.prototype.abort` bridge for signals that have Observable work registered, because JavaScript exposes abort events but not the DOM-standard abort-algorithm hook that must run before those events; - global Web IDL-shaped `Observable` and non-constructible `Subscriber` interfaces. `Observable.from` now follows the pinned platform conversion order: Observable identity, async iterable, sync iterable, then Promise. It no longer accepts arbitrary subscribables at this platform boundary. Sync and async iterators use explicit protocol loops so the fallback can preserve iterator method sampling, `return(reason)`, abort timing, and pending-result behavior that `for await...of` intentionally hides. The structure and behavior pass the pinned Observable WPT revision in window, dedicated-worker, same-origin iframe, and Web IDL coverage. D-045 supersedes D-042: the fallback again enforces the pinned revision's required-argument rule for `Subscriber.next`, and the strict gate permits no product-policy divergence from any selected upstream test. The abort-algorithm bridge is installed only with the fallback constructor. It still patches that realm's `AbortController.prototype.abort`, because JavaScript does not expose the required DOM abort-algorithm hook; controllers without registered Observable algorithms delegate directly to the captured platform method. ### Shared native/fallback lifecycle contract P0.4 adds one self-contained contract under `packages/observable-polyfill/test/lifecycle`. The Node half clears the realm, imports the built package, verifies its frozen fallback marker, and exercises the selected fallback. The browser half serializes that exact contract into a disposable pinned-Chrome session and verifies that the selected constructor is native and unmarked. The contract covers activation and sharing, late joins, individual and last-observer abort, restart, completion, error, synchronous reentrancy, teardown registration and reverse ordering, and host reporting of thrown observer callbacks. The browser execution primitive is shared with the attested WPT harness rather than defining a second browser-selection path. Both the pinned blocking job and latest-Chrome advisory job run the lifecycle contract before WPT. Passing this contract demonstrates agreement only for its bounded lifecycle claims; the pinned WPT suite remains the conformance authority for the broader platform surface. ## Native selection and polyfill boundary ### Accepted target Importing or initializing RxJS must result in one active platform Observable constructor for the realm: - preserve any existing constructor without probing it; - install the fallback only when needed; - never install both as competing identities; - install the paired fallback `Subscriber` only with the fallback constructor; - install `EventTarget.prototype.when` only when `EventTarget` exists and the method is absent. ### Current implementation P0.3 implements D-041: - `import '@rxjs/observable-polyfill'` conditionally initializes the current realm; - every public `rxjs` entry point evaluates that initializer before it touches `Observable`; - `observablePolyfillInfo` uses `Symbol.for('rxjs.observable.polyfill.info.v1')` to address a frozen `{ packageName, version }` marker on an RxJS-installed constructor; - `getObservablePolyfillInfo(constructor = globalThis.Observable)` returns that marker or `undefined` without claiming that an unmarked implementation is native or conforming; - an earlier marked or unmarked constructor wins and is never replaced. The marker property is non-enumerable, non-writable, and non-configurable. Marker-object identity distinguishes two installation instances of the same package version without a UUID or crypto requirement. Initialization performs its checks once when the module evaluates; operators do not poll the marker. All required property changes are preflighted and committed as one transaction. A failed definition rolls back earlier changes; an unsupported frozen target produces a named error instead of leaving a partial realm. Each window, iframe, worker, or server isolate initializes itself. Imports do not walk child realms or transparently extend foreign constructors. Server installation is isolate-global and idempotent rather than per-request. ## Attested Observable WPT harness The WPT harness is a test boundary around the current fallback, not a second installation contract. It does not change production source, exports, ambient types, or the accepted native-first direction. Its purpose is to run the upstream Observable suite in disposable browser realms while proving that each reported result came from the RxJS fallback rather than the browser's native implementation. The written standards reference is WICG/observable commit `d74bace7cf80200a01c81cfe20961e29ac7fa3d8`, specifically `spec.bs`. It is used to understand the rules and diagnose failures; it is not a second executable success gate. The harness pins web-platform-tests/wpt commit `6a009d73f0d315941b90cac13a9523a2a08c631b`. It vendors exactly 29 files from `dom/observable/tentative/`—including the `EventTarget.prototype.when` coverage—and eight derived support files: the license, GC helper, two IDLs, and four WPT harness/parser scripts. The 37 imported files remain byte-for-byte identical to upstream; provenance records each Git blob and SHA-256, and an expected-URL inventory makes missing or duplicated execution a hard failure. Verification derives the support closure from the test sources and rejects both missing dependencies and unexplained extras. Updates use a shallow, blob-filtered sparse checkout and require explicit review of source, dependency, URL, and realm-pattern changes. Execution uses an ignored generated shadow tree: 1. The real polyfill source is synchronously bundled for tests. A manifest records every source hash and the final bundle SHA-256. 2. Before the bundle runs, bootstrap code captures native `Observable`, `Observable.prototype.subscribe`, and `EventTarget.prototype.when` descriptors and references. The pinned blocking browser must expose them, and the disposable realm must permit them to be masked. 3. After fallback installation, a non-enumerable test-only attestation checks that the active constructor, `subscribe`, and `when` references exactly equal the bundle-installed references, differ from the captured native references, and report the expected bundle hash. 4. Generated metadata injects bootstrap and an attestation registrar into every `.any.js`, `.window.js`, HTML, and IDL URL, then registers the one named attestation subtest after upstream source setup has run. This preserves upstream testharness properties such as `allow_uncaught_exception`. The `.any.js` injection is what reaches dedicated-worker variants. Reviewed same-origin `contentWindow` access installs and verifies the same bundle in the child before upstream code uses it. The four iframe URLs cannot pass their single attestation until all nine reviewed child-realm accesses have verified. New realm-creation patterns or child-count drift fail import verification until reviewed. 5. A report auditor independently requires exactly one passing attestation per expected URL. Expectation metadata cannot suppress attestation failures. `pnpm run test:wpt` is the strict conformance gate. It succeeds only when the official browser WPT runner completes, every expected URL runs once, every realm attests exact RxJS identity, the report is complete, and every upstream test and subtest passes. Any failure, error, timeout, or not-run result produces a readable terminal report and a nonzero process exit. `pnpm run test:wpt:baseline` is a separately named harness diagnostic. It compares Observable behavior with the reviewed known-failure baseline while retaining all completeness and identity gates. The baseline is accepted only after three consecutive complete runs agree, and unexpected failures and unexpected passes both reject it. It is not the default test command and is not a conformance claim. The initial narrowed-closure baseline contained 52 generated URLs and 487 reported upstream subtests. All 52 implementation attestations passed. Top-level statuses were 33 `OK`, 15 `ERROR`, and 4 `TIMEOUT`; reported upstream subtest statuses were 314 `PASS`, 159 `FAIL`, 8 `TIMEOUT`, and 6 `NOTRUN`. At P1.4b completion, all 52 URLs reported `OK`, all 525 upstream subtests reported `PASS`, and all 52 implementation attestations passed against Chrome for Testing `150.0.7871.126`. Three further complete attested runs produced identical results before the obsolete failure metadata was removed. D-042 temporarily changed the argument-presence contract; D-045 supersedes it and restores the same complete result as the required current baseline. For bounded network and execution cost, the official sparse WPT runner is cached by WPT revision, operating system, and Python version. Chrome for Testing `150.0.7871.126` and its matching driver are locked by artifact checksum, with warm offline execution required. The blocking job uses fixed concurrency and a wall-clock limit; a non-blocking scheduled latest-Chrome run reports browser drift separately. ## Symbol extension model ### Current pattern Every current exact public extension module: 1. creates and exports a Symbol; 2. augments the global `Observable` or `ObservableCtor` TypeScript interface; 3. assigns an implementation directly to the constructor or prototype under that exact Symbol; 4. creates returned observables through the receiver's construction protocol. D-051 and P4.I1 establish this direct-assignment pattern without changing Symbol identity, public augmentation, construction, or operator behavior. The accepted target instance-extension pattern looks like this: ```ts export const example: unique symbol = Symbol('example'); declare global { interface Observable { [example](): Observable; } } Observable.prototype[example] = function (this: Observable): Observable { const ObservableCtor = this.constructor as ObservableCtor; return new ObservableCtor((subscriber) => { this.subscribe(subscriber, { signal: subscriber.signal }); }); }; ``` Consumers import the Symbol as the stable access key: ```ts import { scan } from 'rxjs/scan'; const totals = source[scan]((total, value) => total + value, 0); ``` ### Native and RxJS operator coexistence The RxJS Symbol catalog is not limited to operators missing from the platform. It also provides Symbols for overlapping names such as `map` and `filter`: ```ts import { map } from 'rxjs/map'; const platformNames = source.map((value) => value.name); const rxjsNames = source[map]((value) => value.name); ``` These are two intentional public surfaces on the same Observable: - `source.map(...)` is the platform-shaped string API. A native implementation owns it when present; the conforming fallback supplies it only when the platform Observable itself is absent. - `source[map](...)` is the RxJS API. Providing it even for an overlapping name lets developers use the same Symbol-based style for the complete RxJS operator catalog. - The RxJS implementation may delegate to the platform method when the contracts match, wrap it, or independently implement additional inputs, overloads, or behavior. Those differences are part of the RxJS contract and require focused documentation and tests. Delegation must also preserve the receiver's approved `[create]` policy; it cannot turn a ColdObservable Symbol result into a platform Observable as an accidental fast path. - Installing the RxJS Symbol must never replace or alias over the string-named platform property. The same ownership rule applies in fallback mode: the fallback's `.map` remains the platform-conformance surface, while `[map]` remains a separately versioned RxJS extension. A shared familiar name is not a promise that every overload, type, or edge case is identical. ### Why side-effect patching is collision-safe The use of Symbols changes the risk profile of prototype patching. A unique Symbol behaves like an unforgeable property key for ordinary collaboration: ```ts const first = Symbol('scan'); const second = Symbol('scan'); first === second; // false ``` The description is only a debugging label. A string property named `"scan"`, or a different Symbol also described as `"scan"`, cannot overwrite the implementation stored under `first`. Code must possess the exact Symbol value to read or replace that property. That gives each exported RxJS Symbol a deliberately narrow authority boundary: - importing the extension module installs an implementation in that Symbol's slot; - importing the exported Symbol lets a consumer invoke that capability; - an unrelated library can patch its own Symbol without colliding, even if it uses the same description; - code that has deliberately received the RxJS Symbol can replace that one implementation, but it cannot accidentally trample other Symbol extensions. This directly addresses a deficiency of the RxJS 5 `rxjs/add/operator/*` design. Those modules patched string-named properties such as `Observable.prototype.map`. The string name was shared global territory, so another library, another RxJS copy, or a different version could replace the method based on import order. Symbol-keyed patching removes that accidental shared namespace while retaining the convenience of import-time installation. This is collision isolation, not a security boundary. Reflective code can enumerate Symbol properties, and an exported Symbol is intentionally available to its importers. The guarantee is that unrelated code cannot collide merely by choosing the same name. The guarantee is strongest with unique Symbols, which remain the rule for public RxJS operators and factories. `Symbol.for(key)` deliberately uses a shared global registry: any code that knows `key` can recover the same Symbol and write to the same slot. The one accepted exception is the internal construction protocol in D-037. Compatible copies use `Symbol.for('rxjs.kernel.create.v1')` so an operator from one copy can discover the construction policy of an Observable subclass from another copy. The ABI version belongs to the protocol rather than the package release. Installation keeps an existing callable implementation and rejects an occupied non-callable slot. No public operator Symbol becomes globally recoverable as a result. ### Constructor preservation `create.ts` installs the receiver's versioned `[create]` protocol, and operators and factories invoke that protocol directly for derived results. The inherited implementation preserves a same-realm subclass whose constructor accepts the platform initializer shape; an explicit override may select another result contract. `ColdObservable` uses that seam to construct another plain `ColdObservable`. Its native string-named methods instead delegate through a fresh base Observable and return platform Observables. Static Symbols follow their static receiver. Incompatible constructors and generic borrowing onto unrelated objects are unsupported. Transparent cross-realm operation is not supported: each realm initializes its own constructor and extensions. Input conversion is deliberately independent. Operator inputs use the active realm's platform `Observable.from`, preserving its accepted input categories, ordering, identity, and errors rather than asking a result subclass to redefine normalization. Source subscriptions inherit the derived subscriber's signal; operator-local early-cancellation boundaries join their own controller with that signal. One positional internal source-subscription helper binds default forwarding callbacks, wraps operator overrides, routes synchronous setup failures through the source-error path, and joins an optional local signal. Synchronous callbacks, host setup, and conversions are forwarded as stream errors, while downstream observer exceptions retain platform host reporting. Raw subscriptions remain at root-core and Subject-like connection boundaries, async-generator adapters without a destination Subscriber, lifecycles retained beyond the outer result, and terminal/finalization paths whose ordering or host-reporting behavior would be changed by destination-signal ownership. ### Installation side effects An extension import mutates `Observable` or `Observable.prototype`. That makes the following architectural concerns inseparable from the API: - the active constructor must exist before the module evaluates; - duplicate package copies must agree on Symbol identity or remain isolated in a documented way; - tree-shaking metadata must not incorrectly erase required installation; - patching may fail for non-extensible constructors or prototypes. Every public `rxjs` root or subpath import first evaluates the conditional polyfill initializer for its realm. The root then installs only the shared construction kernel needed by its non-operator core exports. An operator or factory subpath installs only its own exact Symbol capability plus required kernel dependencies. The root does not install the complete operator catalog. The package declares `sideEffects: true`; direct subpaths supply the intended capability granularity while preventing a bundler from erasing acquisition or installation merely because an imported Symbol binding is unused. Browser windows, worker realms, Node, Deno, and Bun are supported only through the exact D-053 matrix when the required web primitives exist. Hardened globals and non-extensible constructors or prototypes are outside the initial claim. Public extension installation may surface the native assignment error and a paired static/instance capability has no transactional guarantee on those unsupported targets. Other edge runtimes remain unclaimed until tested. P2.1 accepts D-048: public extension Symbols are exact and module-owned. An independently evaluated duplicate package copy or other version receives a different public Symbol and installs a separate slot. RxJS 9 `import` and Node `require(esm)` instead share the same ESM module identity. Only the versioned construction ABI remains shared across compatible copies. P2.2 historically added a common internal installer for those public slots. It preflighted every requested constructor and prototype property, treated the identical value as an idempotent installation, rejected an occupied exact key, and rolled back earlier definitions if a later definition failed. Installed properties were non-enumerable, writable, and configurable. Missing capacity and mutation failures produced capability-named diagnostics. D-051 and P4.I1 supersede that installation mechanism. P3.3 completed the historical catalog adoption. P4.I1 then migrated all 97 current exact public extension Symbols to direct assignment, including async-iteration Symbols that return generators instead of Observables. The common installer and its installer-only tests no longer exist. A blocking source audit now requires each public capability to assign its exported exact Symbol on the declared constructor or prototype and rejects RxJS-specific string-named additions or a return to the installer abstraction. D-051 supersedes that installation mechanism while preserving D-048's identity, realm, and bundling policy. Exact module-owned Symbols already isolate unrelated libraries, package copies, and versions; normal module caching handles repeat evaluation of one module instance. The accepted target is direct assignment to the constructor or prototype under the module's own exported Symbol. It deliberately removes runtime preflight, collision diagnostics, descriptor customization, extensibility checks, and rollback. P4.I1 completed the code, test, audit, and bundle-evidence migration. Direct assignment uses the ordinary writable, enumerable, configurable descriptor; unsupported hardened targets receive no RxJS-specific preflight, rollback, or diagnostic. ### Public Symbol consistency Public extension modules use `Symbol('name')`, producing a key unique to that module evaluation. The former unreviewed `Symbol.for('buffer')` exception has been removed, and public declarations use explicit `unique symbol` types. `create.ts` continues to use the accepted namespaced global protocol key from D-037. D-048 records the version, duplicate-copy, realm, and collision consequences of that split. ## Current API inventory This inventory documents what exists in source, not a supported public API. ### Platform-shaped fallback surface - Constructor and subscription: `Observable`, `Observable.from`, `Observable.prototype.subscribe` - Observable-returning methods: `takeUntil`, `map`, `filter`, `take`, `drop`, `flatMap`, `switchMap`, `inspect`, `catch`, `finally` - Promise-returning methods: `forEach`, `first`, `last`, `find`, `some`, `every`, `reduce`, `toArray` - Event integration: `EventTarget.prototype.when` - Subscriber surface: `next`, `error`, `complete`, `addTeardown`, `active`, `signal` ### Symbol extensions in `packages/rxjs` | Placement | Current extensions | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Static and instance | `create`, `combine`, `combineLatest`, `concat`, `merge`, `onErrorResumeNext`, `pipe`, `race` | | Static | `animationFrames`, `forkJoin`, `generate`, `interval`, `partition`, `timer` | | Instance | `buffer`, `bufferTime`, `catchError`, `combineLatestAll`, `count`, `debounce`, `defaultIfEmpty`, `delay`, `distinct`, `distinctUntilChanged`, `distinctUntilKeyChanged`, `elementAt`, `every`, `exhaustMap`, `expand`, `filter`, `finalize`, `find`, `findIndex`, `first`, `isEmpty`, `iterateBufferedValues`, `iterateEachValue`, `iterateLatestValue`, `iterateNextValue`, `last`, `map`, `max`, `mergeMap`, `min`, `observeOn`, `pairwise`, `pluck`, `reduce`, `repeat`, `retry`, `sampleTime`, `scan`, `sequenceEqual`, `single`, `skip`, `skipLast`, `skipUntil`, `skipWhile`, `startWith`, `subscribeOn`, `switchMap`, `take`, `takeLast`, `takeUntil`, `takeWhile`, `tap`, `throttle`, `throwIfEmpty`, `timeInterval`, `timeout`, `timestamp`, `windowCount`, `windowTime`, `withLatestFrom`, `zipAll`, `zipWith` | The exact RxJS `map`, `filter`, `first`, `last`, and `find` Symbols coexist with the fallback's same-familiar-name string methods. Installing an RxJS extension does not replace or widen the platform method; callers select the contract through the property key. ### Intentional non-operator primitives - `Subject`, including a class-local `asObservable()` method that returns a distinct non-mutating view through the Subject's platform Observable base - `ColdObservable` - `PerSubscriptionSubjectBase`, an advanced abstract base for hot Subject variants that require setup for every direct subscription - `behaviorSubject` - `replaySubject` - `TimeoutError` - `zip` - experimental fake timers and `ScheduledObservable` These APIs remain subject to focused public-contract review, but they belong to `rxjs` rather than a separate compatibility product. Passing an RxJS 7 test against one of them does not imply source, type, import, or lifecycle compatibility with RxJS 7. The four exact async-iteration Symbols convert the receiver's push notifications into a fresh, lazy, one-shot async generator. `iterateEachValue` retains a lossless FIFO queue; `iterateBufferedValues` yields lossless microtask-coalesced snapshots; `iterateLatestValue` retains only the latest unread value; and `iterateNextValue` accepts only the first value that arrives while the generator has an outstanding request. The last two strategies are deliberately lossy. A synchronously completing source therefore yields every value through `iterateEachValue`, one batch through `iterateBufferedValues`, only its final value through `iterateLatestValue`, and no values through `iterateNextValue`. Each generator owns its own queue, buffer, latest-value slot, or demand slot, but it subscribes directly to its receiver. Concurrent generators over a platform Observable are separate observers of the same shared, ref-counted active producer. A generator that joins late sees only future source notifications; closing one generator leaves the producer active while another observer remains, and a later iteration after ref-count closure starts a new producer run. The same Symbol methods on `ColdObservable` instead activate one independent producer per generator because its direct `subscribe()` contract is producer-per-subscription. Iteration starts the subscription only when the generator is first advanced. Generator cleanup aborts its observer when a loop breaks, its body throws, or the generator is explicitly closed. Accepted queued, buffered, or latest values are yielded before source completion or error becomes visible. The former standalone `eachValueFrom` and `bufferedValuesFrom` source modules and package subpaths were removed rather than retained as aliases. Notifier gates, synchronous queries, property selection, prefix/pair sequencing, and partitioning keep one state machine per active platform producer run. Concurrent observers share that state, while restart after the ref count reaches zero begins with fresh state. Early terminal queries cancel synchronous upstream work before delivering their result. The two static `partition` branches retain independent predicate and index state over the same platform-converted input. `windowCount` emits its first read-only window before source activation and supports tumbling, overlapping, and gapped cadence. Source completion completes live windows, source errors error them, and outer cancellation silently releases them without converting cancellation into completion. Synchronous `generate` and recursive `expand` likewise keep one activation-scoped state machine. `expand` uses FIFO concurrency and iterative draining, so synchronous recursion does not grow the JavaScript stack. The numeric form of the Symbol-keyed `debounce` uses host timers rather than an RxJS scheduler. Each source value replaces the pending timer, normal source completion flushes the pending value immediately before completing, and result cancellation clears the timer through the platform subscriber lifecycle. Concurrent fallback observers share that one active timer and source subscription; numeric debounce does not introduce producer-per-observer work. The exact `delay` Symbol schedules each value with a host timeout, delays normal completion until every pending value is delivered, and forwards source errors immediately. A `Date` supplies one absolute release boundary. The exact `sampleTime` Symbol likewise uses a host interval and emits only the latest value received since the preceding tick. `timestamp` and `timeInterval` read `Date.now()` by default and accept the narrow timestamp-provider contract required by their RxJS 7 evidence; they do not introduce a general scheduler abstraction. `animationFrames` uses the host animation-frame callback timestamp for its default `timestamp`, derives `elapsed` from the active timestamp provider, and cancels through the platform subscriber lifecycle. The unified `timeout` Symbol supports initial and per-value host deadlines. Expiry aborts the source before activating the configured fallback; absent a fallback it errors with the exported `TimeoutError` and its `seen`, `lastValue`, and `meta` context. Source completion, source error, and result cancellation all cancel pending timeout work through `AbortSignal`. The exact `bufferTime` Symbol uses host timers for sequential or overlapping buffers. Sequential buffers restart their span after a size-triggered close; overlapping buffers retain the configured creation cadence. Completion flushes each active buffer in creation order, errors discard them, and cancellation clears all close and creation timers. The exact `windowTime` Symbol follows the same host-time cadence while exposing read-only Subject views. Sequential windows reopen after time- or size-based closure; overlapping windows retain their creation cadence. Source terminal events terminate active windows before the outer result, while outer cancellation releases windows silently. The exact `observeOn` and `subscribeOn` Symbols use host timeouts instead of a public scheduler. `observeOn` defers notifications in source order; `subscribeOn` defers source activation. Both cancel pending work through the derived subscriber's `AbortSignal` and retain one shared, ref-counted platform activation for concurrent observers. All platform-layer host work resolves its scheduling and cancellation functions from `globalThis` at the moment work is scheduled or cancelled. RxJS-owned provider/delegate objects and module-evaluation captures are not part of the Next runtime boundary. This lets the same `@rxjs/test` realm patch virtualize RxJS operators and ordinary application host calls. `Subject.asObservable()` does not add a string-named method to `Observable.prototype`. The returned base-Observable view forwards `next`/`error`/`complete` from the Subject, delegates cancellation through the derived subscriber's `AbortSignal`, and therefore retains the selected base Observable's lifecycle. With the platform fallback, concurrent view observers share one active forwarding subscription and ref-count it; the view does not recreate RxJS 7 producer-per-subscription behavior. The standalone `zip(sources)` retains one FIFO buffer per input. Without its non-RxJS `fillAfterComplete` option, it completes as soon as any completed input has no buffered value left, because no further complete tuple is possible. This includes an input that completes empty and the point immediately after a final tuple drains a previously completed input. Result completion, input error, and last-observer cancellation close every active input through the result subscriber's `AbortSignal`; concurrent platform observers share one ref-counted zip activation. Synchronous termination stops activation of later inputs. With `fillAfterComplete`, a completed empty input instead contributes the configured fill value while another input still has a real buffered value; the result completes after every input is complete and all real buffered values have been drained. An empty source list completes immediately. ## Test architecture `@rxjs/test` is a separate development-time package. Its `rxTest` function owns the virtual-time engine, redirects supported realm scheduling APIs for the complete async test lifetime, evaluates registered expectations, and restores the original property descriptors in every exit path. The API makes lifecycle semantics explicit: - `cold()` creates an independent producer during each subscription; - `hot()` creates a subject-like absolute-timeline producer before subscriptions; - `observable()` follows the platform's shared/ref-counted active producer lifecycle: the first subscription creates a producer, concurrent subscriptions join it, and a later subscription after ref-count closure creates another. The package does not expose `TestScheduler` or add scheduler arguments to the main library. Its public `ColdObservable` dependency conditionally initializes the fallback when the realm has no Observable and preserves an existing native constructor. See `docs/rxjs-next/TESTING_DESIGN.md`, D-012, D-034, and D-043. The RxJS 7 marble-test evidence is maintained separately under `packages/rxjs/test/ported`. A source-pinned manifest records one disposition for each of 2,338 registrations expanded from 2,201 physical declarations, including parameterized variants, source-skipped declarations, missing APIs, and obsolete scheduler internals. Every record has a unique case ID, executable program, and cold parity registration; unavailable capabilities fail explicitly instead of removing the source case from collection. The repository owns 147 formatted Vitest `.spec.ts` files for each of the cold and platform modes. They were produced by a one-time migration and are now normal checked-in source: each file imports `rxTest` and public RxJS Symbols directly, and each test has a real repository filename and line number. Modes run in isolated Vitest processes so the platform constructor is selected before extension modules load. All 2,338 definitions are registered in each available mode. Cold mode installs the fallback platform base without replacing the global with `ColdObservable`. Its `cold()` fixtures extend `ColdObservable`, and explicit cold constructors and static factories name `ColdObservable` directly. `hot()` fixtures extend the active global constructor and derive ordinary platform Observables; `observable()` directly uses the active global constructor and its shared/ref-counted lifecycle. Platform modes use the ambient `globalThis.Observable`; tests never import a fallback constructor. The RxJS `test:unit` gate registers every ported case as an ordinary test in cold and polyfill modes: a converted-program failure, missing API, unsupported harness dependency, source-skipped case, or exact duplicate fails the command instead of being quarantined or inverted with an expected-failure wrapper. Vitest's unmodified built-in default reporter provides human output and real clickable locations. The CI verifier captures Vitest's verbose per-case result stream; the static migration report maps declaration order back to manifest case IDs without putting machine identifiers in test names. Tests use ordinary Vitest assertions and spies without a compatibility assertion layer. Dedicated platform cases assert the shared/ref-counted lifecycle directly. Native loading also verifies that the ambient constructor was not replaced. The recorded mode baselines remain diagnostic evidence and do not change default test outcomes. Under D-055, a separate CI verifier executes the complete per-case audits and requires their exact case-ID pass sets to match the reviewed 2,299/39 cold and 2,316/22 polyfill baselines. Both regressions and unexpected passes block CI so evidence and classifications cannot drift silently. See `RXJS_7_MARBLE_TEST_PORT_NOTES.md`, D-013, and D-055. RxJS 7 helper inputs that expose only a lowercase `subscribe` method or legacy interop protocol remain unchanged in the checked-in migration evidence. They are classified as `compatibility-only` and fail explicitly where the current surface rejects arbitrary subscribables. Replacing those inputs with platform Observables would change the behavioral claim rather than preserve it. P0.M1 established an exploratory `@rxjs/migrate` package. P0.M3 hardened its framework-neutral semantic transform, versioned capability registry, Mocha/Chai-to-Vitest adapter, structured dry-run-first CLI, contract schemas, safe batch writes, package gates, and canonical Skill integrity primitives. D-046 narrows the accepted product to that deterministic engine and the single canonical Skill, while thin Codex, Claude Code, and Cursor adapters expose the same versioned Skill. The former MCP prototype, bin, export, dependency, tests, and claims are removed. Framework syntax remains an adapter boundary, so projects may preserve their current runner or add another source/target pair without changing `rxTest` semantics. The repository's native/polyfill execution matrix remains local test infrastructure, not generated user code. See `packages/migrate/docs/MIGRATION_TOOLING_DESIGN.md`. ### Agent-first migration architecture The migration Skill owns project discovery, baseline capture, behavioral classification, migration-contract approval, bounded execution, repair, and closeout. Before changing source, it records each affected pipeline as `platform-shared`, `producer-per-direct-subscription`, `subject-hot`, `not-applicable`, `unsupported`, or `unresolved`. Unsupported or unresolved behavior, missing characterization evidence, and lifecycle-sensitive choices remain visible escalation points rather than transform defaults. The deterministic engine may parse source, apply reviewed capability mappings, adapt framework syntax, and return diagnostics. It must not choose lifecycle semantics, manufacture missing evidence, or declare a project migrated. A mechanical fixture lane now proves transform, diagnostics, source and target type checks, pinned RxJS 7 and Next behavior, path containment, dry-run/write, idempotence, imports, and packed publication properties. A separate agent evaluation lane proves reviewed outcomes from the same canonical Skill digest. P0.M5 qualifies that lane only for Codex/ChatGPT; Claude Code and Cursor retain P0.M4 installation and discovery evidence but no measured migration-outcome claim. `packages/migrate/docs/MIGRATION_TOOLING_DESIGN.md` is the controlling product and validation contract. The 2026-08-01 qualification snapshot ran four pinned RxJS 7 repositories through Codex `0.146.0-alpha.3.1` with `gpt-5.6-sol` at medium reasoning. All four passed the 14 semantic gate families: three completed their approved migrations and the weak-coverage/unsupported scenario made its required safe stop before target installation or migration writes. The records bind `@rxjs/migrate` and the canonical Skill to `8.0.0-alpha.14`, retain five SHA-256-addressed artifacts per run, and are verified offline. This is bounded evidence for those scenarios and settings, not a general automatic-migration or cross-harness reliability claim. `docs/rxjs-next/RxJS-7-parity.md` is the generated public-surface map. Its machine-readable capability registry distinguishes instance operator Symbols, static factory Symbols, ambient-platform constructions, and standalone values. Pipeable migration invokes `source[targetSymbol](...adaptedArgs)`: exact operators retain their arguments, while explicitly recorded unified mappings such as `bufferCount → buffer({ maxSize, startEvery, emitRemainingOnError: false })` and `bufferWhen → buffer({ delay: closingSelector, emitEmpty: true, emitRemainingOnError: false })` adapt old signatures. The count-window configuration starts an initial buffer when the producer activates, supports overlapping or gapped windows, emits full buffers as they reach `maxSize`, and emits remaining non-empty buffers in creation order on completion. Supplying `startEvery` selects this count-window mode. When it is omitted, delay-window buffering evaluates its closing selector before activating source work; a synchronous selector failure errors the result without activating the source. The `bufferWhen` mapping discards an active partial buffer when the source errors. A similarly named platform string method is not treated as RxJS Symbol parity. RxJS 7 `buffer(closingNotifier)` uses the same delay-window mode with `restartDelay: false`, retaining one notifier subscription across boundary values while the default delay-selector mode restarts after every boundary. RxJS 7 `audit(durationSelector)` and `throttle(durationSelector, config)` share the Symbol-keyed `throttle` implementation. A duration value closes a window; duration completion only cleans it up and does not emit a trailing value. Throttle starts a new duration after a trailing emission, while the audit adapter sets `leading: false`, `trailing: true`, and `restartOnTrailing: false` so the next source value starts the next audit window. Source completion waits only when an active duration owns a pending trailing value. Direct identity mappings cover `takeUntil`, `skipUntil`, `pluck`, `find`, `findIndex`, `throwIfEmpty`, `isEmpty`, `startWith`, `pairwise`, and `windowCount`. `partition` and `generate` map to exact static Symbols. RxJS 7 `expand(project, numericConcurrency)` maps to `source[expand](project, { concurrent: numericConcurrency })`. Reusing one projected platform fixture during recursion retains the case-scoped shared/ref-counted expectation from D-013 rather than manufacturing a cold inner. Legacy scheduler forms of `startWith`, `generate`, and `expand` are not public platform contracts. Their ported behavioral claims execute through explicit `@rxjs/test` scheduling rewrites at the migration-evidence boundary; see D-033. ## Intentional producer-per-subscription APIs ### Why the distinction is necessary An ordinary RxJS 7 cold Observable creates a separate producer execution during each subscription. The platform Observable creates one producer for its first subscription and shares that active producer among current observers. Changing the platform layer to recreate RxJS 7's producer-per-subscription model would violate the project foundation and make native and polyfilled behavior diverge. D-039 rejects a separate RxJS 7 compatibility runtime. RxJS Next can still publish a type with a producer-per-direct-subscription contract when that behavior is useful and explicit. Such a type is an intentional Next API, not a facade that promises RxJS 7 imports, subscriptions, pipeable operators, schedulers, or types. ### Current prototypes `ColdObservable` subclasses the active platform Observable, overrides `subscribe()`, and creates a new `ColdSubscriber` per direct JavaScript call. It also defines the shared versioned `[create]` protocol so RxJS Symbol operators return plain ColdObservables. Its string-named platform methods take the opposite path: each delegates through a fresh base Observable view, so Observable-returning methods return platform Observables and Promise consumers still activate the cold source correctly. `PerSubscriptionSubjectBase`, the behavior-subject factory, and the replay-subject factory build on that direct-subscription mechanism. `PerSubscriptionSubjectBase` is an advanced abstract base rather than an ordinary Subject for application code. Like every Subject, its instance is a hot producer that exists before its observers subscribe. “Per subscription” describes observer-local setup, not producer creation. Its material differences from `Subject` are: - `Subject` extends the platform Observable. Concurrent observers can share one active platform `Subscriber`, which the Subject stores as one fanout destination. - `PerSubscriptionSubjectBase` extends `ColdObservable`, so every direct call to `subscribe()` creates a separate `ColdSubscriber` and calls the protected `_subscribe` hook. - `BehaviorSubject` and `ReplaySubject` currently use that per-observer hook to emit a current value or replay buffered values to each late observer. Merely changing their base class to the current `Subject` would skip that hook when a late observer joins an already-active platform subscription. - The base has separate input/output type parameters. A subclass using different types owns the safety of that conversion; `Subject` has one value type. - `Subject` provides `asObservable()` and overrides the Symbol-keyed creation hook so operator results use the immutable platform Observable base. `PerSubscriptionSubjectBase` instead inherits the cold `[create]` protocol. Symbol-operator results are plain ColdObservables rather than mutable Subject subclasses, while native-method results are platform Observables. The constructor is protected and the class is abstract so it cannot be presented as a beginner-facing replacement for `Subject`. Its default `_subscribe` implementation handles retained terminal events and live fanout. Subclasses normally perform their observer-local setup and delegate to that implementation. A subclass that calls the lower-level `addSubscriber` helper instead owns terminal handling, replay ordering, active-state checks, and teardown correctness. The hook is deliberately a **direct-subscription hook**. `ColdObservable`'s explicit native-method overrides reach it by subscribing the fresh platform view to the cold source. For a behavior or replay subject, that means observer-local setup runs once for each active platform view, while concurrent observers of the same native result share that platform activation. Borrowed or newly introduced native methods are not automatically covered; the method-inventory test must identify and force review of platform-surface growth. For a plain Subject, the extra per-subscription plumbing has no identified fanout purpose. The current behavior- and replay-subject prototypes are its concrete subclasses because they need observer-local replay. The former `ColdSubject` name was removed rather than retained as an alias; it incorrectly implied a cold producer and obscured the base-class intent. D-050 stabilizes these classes and factories in the main `rxjs` package as intentional Next APIs. Their public declarations expose Symbol-derived results as `Observable` even when D-037 selects a `ColdObservable` at runtime; the type surface does not encode producer lifecycle. Cancellation remains AbortSignal-based, Subject terminal and replay behavior is covered directly, and none of these contracts creates an RxJS 7 compatibility claim. See `COMPATIBILITY.md` for the migration-evidence policy. ## Package and import architecture ### Current package facts - All current package manifests report `9.0.0-beta.0`. - `packages/observable` and its workspace-preparation references are removed. - `rxjs` declares an exact runtime dependency on `@rxjs/observable-polyfill`. - Every public `rxjs` source entry reaches the conditional initializer before reading or extending `Observable`. - The root source exports the approved non-operator core. Each public source subpath has one ESM runtime and declaration export. - The polyfill's ambient declarations are emitted from its package entry. - All four release packages build one ESM output without self-links or source specs in the packed artifact. Browser, Webpack, `import`, and Node `require(esm)` conditions share that output where applicable. - Repository metadata names each package's actual directory. - ESM, Node `require(esm)`, declaration-consumer, bundler, and per-realm import fixtures exercise the package map. D-053 defines the final support matrix. ### Accepted package map and import behavior The published runtime map has three products: | Package | Accepted responsibility | | --------------------------- | -------------------------------------------------------------------------------------------------- | | `@rxjs/observable-polyfill` | Independently publishable conditional fallback and owner of the base ambient platform declarations | | `rxjs` | Symbol extensions plus intentional non-operator RxJS Next classes and values | | `@rxjs/test` | Implementation-neutral test harness that consumes an already initialized realm | `@rxjs/observable` has no target role and is removed. No runtime compatibility package replaces it. `rxjs` declares a runtime dependency on `@rxjs/observable-polyfill`. Every public root or subpath import first evaluates the conditional initializer. The root exports non-operator core values—cold, Subject, connectable, notification, and public-error primitives—without installing the operator and factory catalog. An operator or factory subpath installs only its exported exact Symbol capability and internal kernel dependencies. The polyfill package owns the ambient TypeScript declarations for `Observable`, `Subscriber`, `ObservableValue`, and `EventTarget.when`. Individual `rxjs` entry points augment those base declarations only with the Symbols they export. `@rxjs/test` imports the public `ColdObservable` entry; that entry preserves an existing constructor or conditionally initializes the fallback when the realm is empty. ### Supported release environments D-053 selects one published ESM implementation for every supported target. Node `22.13.0+` on the Node 22 line and maintained Node 24 are blocking; Node 26 is advisory during beta. Latest stable Chrome and Firefox, current desktop Safari, current Mobile Safari in an iOS simulator, current stable Deno and Bun, and Webpack 5 are blocking. The pinned Chrome 150 WPT lane remains the reproducible conformance authority; current browser lanes detect integration and upstream drift. Browser, Webpack, `import`, and Node `require(esm)` conditions point to the same `dist/esm` files and declarations. There is no CommonJS or target-specific code copy. The Node bridge is a supported transition on the declared Node range, not a CommonJS artifact or a promise to legacy resolvers. Deno and Bun consume the unchanged npm package. Tests for those environments must not introduce shims, runtime branches, dependencies, or bundle bytes. Supported runtimes supply `WeakRef`, `AbortSignal.any`, `Symbol.dispose`, `EventTarget`, and applicable DOM types. The accepted error-reporting fallback continues to cover hosts without `globalThis.reportError`. Other edge runtimes, hardened globals, non-extensible installation targets, and transparent cross-realm operation remain unclaimed. ### Documentation ownership D-052 keeps package-relative user documentation inside the package it describes. The RxJS 7 migration guide and its generated evidence references therefore live under `packages/rxjs`; migration-engine and canonical-Skill documentation lives under `packages/migrate`; and testing-package documentation belongs under `packages/test`. Repository-wide charter, architecture, decisions, open questions, compatibility policy, and active-plan records remain under `docs/rxjs-next`. The root README is the repository entry point and may link to those package containers. `apps/rxjs.dev` is maintained by a separate workstream and is not edited, built, tested, published, or otherwise used as a delivery surface by this project plan. Future website integration requires an explicit coordinated change after the package documentation stabilizes. ### Target dependency direction The runtime dependency direction is acyclic: ```mermaid flowchart TD Standards["Pinned Observable spec and WPT baseline"] --> Polyfill["@rxjs/observable-polyfill"] Polyfill --> RxJS["rxjs core and Symbol subpaths"] RxJS --> Test["@rxjs/test"] Active["Active realm Observable"] --> Test["@rxjs/test"] RxJS -.->|conditionally initializes realm| Active RxJS -.-> Migrate["@rxjs/migrate development tool"] Test -.-> Migrate Skill["Canonical migration Skill"] --> Migrate ``` The fallback must not depend on RxJS operators or migration tooling. `@rxjs/test` preserves an existing native constructor and otherwise receives the fallback through its public RxJS cold dependency. ## Repository workspace and tooling The repository uses pnpm 10.34.5 for local development, workspace execution, CI, and release preparation. `pnpm-workspace.yaml` is the authoritative workspace definition for the four packages under `packages/*` and the `apps/rxjs.dev` application; the root project provides shared tooling, making six install projects in total. pnpm's default isolated linker keeps package-local type dependencies separate without a public-hoist bridge. The docs application continues to resolve its declared RxJS 7 dependency from the registry rather than linking the exploratory local `rxjs` package. Dependency build scripts use a version-bounded allow/deny policy with `strictDepBuilds` enabled. Newly introduced install scripts therefore require explicit review. CI installs the committed pnpm lockfile with `--frozen-lockfile`. Repository scripts declare dependencies they use directly; in particular, RxJS tests declare Chai and the release helper declares Yargs instead of depending on a flat installation layout. A version-pinned patch changes Husky 4's generated pnpm hook runner from the obsolete `pnpx --no-install` form to `pnpm exec`. CI has four durable ownership layers. Main CI runs focused package behavior, lint, builds, declarations, imports, publication fixtures, runtime contracts, the exact migration-evidence audits, bundle-analysis tests, SafariDriver unit tests, and active-workflow validation on pull requests and `master`. TypeScript-latest also runs in both contexts. Pinned Observable WPT and the full browser, Webpack, performance, adoption, Deno, Bun, desktop Safari, and Mobile Safari release-readiness matrix are path-aware on pull requests and unconditional on `master`. The latest-Chrome drift lane and Node 26 remain advisory; other accepted release lanes are blocking. Release coherence guards the commands, environment matrix, and `master` triggers. ## Build and test baseline Verified on 2026-07-24 from commit `9e94c090e`: | Check | Result | Interpretation | | ------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Polyfill source tests | 4 unique source tests pass | Covers global installation, basic next/complete teardown, error flow, and `EventTarget.when`; not conformance | | RxJS source tests | 1 test passes | Covers one `scan` example only | | Polyfill package build | Fails | Ambient platform declarations are not visible to the build entry, causing missing global types and follow-on errors | | Polyfill package lint | Fails | The existing ESLint project points at `packages/observable/tsconfig.json`, which does not include the polyfill sources | | RxJS package build | Fails | `tshy` rejects the array-valued root export configuration before compilation | | Workspace project discovery | Passes with the Nx daemon disabled | Discovers `@rxjs/observable-polyfill`, `@rxjs/observable`, `rxjs`, and `rxjs.dev` | | Attested Observable WPT harness | Strict command fails on current conformance gaps; explicit baseline diagnostic passes | 37-file approved closure, 52 generated URLs, 52 passing exact-identity attestations, readable terminal failures, three identical baseline runs, and a warm offline rerun | The P0.T3 parity baseline was verified on 2026-07-29. The strict `pnpm --filter rxjs test` command passed 705 focused source tests, then passed all 2,338 registrations in both cold and polyfill modes. The durable ledger retains all 1,923 cases that failed an earlier complete audit and marks every row `FIXED`. The final manifest contains 1,503 active, 831 compatibility/expected-failure, and 4 exact-deduplicate registrations; those dispositions remain classification metadata and do not weaken ordinary test semantics. The P0.3 package baseline was verified on 2026-07-30 with Node `24.12.0`: | Check | Result | | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | Frozen-lockfile install and workspace discovery | Passes; five install projects and four Nx projects, with only the three accepted runtime packages | | Workspace publication preparation | Passes builds and lints for `@rxjs/observable-polyfill`, `rxjs`, and `@rxjs/test` | | Package fixtures | All three packages pass clean multi-dialect builds, declaration consumers, ESM imports, and CommonJS imports | | Conditional installation fixtures | Pass missing-global, marker, foreign/earlier constructor, independent `when`, direct-subpath, core-only root, worker-realm, and frozen-target cases | | Published-file dry runs | Contain `dist` runtime/declaration artifacts plus package metadata; no source specs or generated self-links | | Polyfill and test-package source suites | 49 polyfill/harness tests and 75 `@rxjs/test` tests pass | | Attested Observable WPT harness | 52/52 URLs, 525/525 upstream subtests, and 52/52 exact RxJS identity attestations pass; no failure expectations or skips | Rebuilding the formerly disconnected polyfill entry also exposed that the historical focused RxJS source-test baseline had been consuming a stale fallback artifact. P0.4 reconciled the focused suite to 733/733 passing tests against the rebuilt fallback. After removing invalid wrappers that had replaced RxJS 7 arbitrary-subscribable inputs, the Phase 3 complete cold migration audit now passes 2,299/2,338 and the fallback audit passes 2,316/2,338. The 39 cold failures are 24 explicit D-013/D-043 lifecycle divergences and 15 compatibility-only arbitrary-subscribable inputs. The 22 fallback failures are seven of those lifecycle divergences and the same 15 compatibility-only inputs. No portable or harness-rewrite failure remains unexplained, and neither audit has a skipped or pending registration. Because the default ported command intentionally has no expected-failure quarantine, the all-mode RxJS command remains nonzero while this evidence stays executable. The package-independent lifecycle contract itself passes against both the packaged fallback and native Observable. The P2.4 extension-kernel baseline was verified on 2026-08-01: | Check | Result | | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Focused RxJS source suite | 756/756 tests pass | | Kernel source and type contract | The common installer/helpers and all six pilot capabilities pass strict source typing and focused descriptor, construction, sharing, cancellation, terminal, and error tests | | Package and bundler contract | Builds, declaration consumers, ESM/CommonJS imports, duplicate-dialect coexistence, frozen-target failure, root tree-shaking, and retained direct extension imports pass | | Native/fallback kernel contract | The same eight cases pass against the packaged fallback and native Observable in Chrome `150.0.7871.126` | | Targeted migrated evidence | 97/98 pilot registrations pass in cold mode and 97/98 pass in fallback mode; only the classified compatibility-only `switchMap` arbitrary-subscribable input remains unsupported by D-049 | | Complete migrated evidence | 2,296/2,338 cold and 2,321/2,343 fallback registrations pass; the remaining restoration/compatibility backlog is outside the extension-kernel phase | That historical baseline proved the P2.4 common extension pattern, not RxJS 7 runtime compatibility or completion of the operator catalog. The strict all-mode ported command therefore remains intentionally nonzero while Phase 3 and Phase 4 classify and resolve the remaining API work. The P3.4 restoration audit supersedes the complete-migrated-evidence row for the current 2,338-case corpus: 2,299 cold and 2,316 fallback cases pass. Every remaining failure is explicitly `intentional-divergence` or `compatibility-only`; the reviewed case-ID baselines and generated migration ledger are the authoritative current evidence. P4.I1 supersedes the installer-specific portions of the P2.4 baseline. All 97 exact public Symbols now use direct assignment, and the following current verification passed on 2026-08-01: | Check | Result | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Focused RxJS source suite | 106 files and 750 tests pass; the six removed cases were installer-only transactional tests | | Direct-installation source audit | All 97 exact public Symbols install on their declared static or instance target; installer references and RxJS-specific string additions are rejected | | Type and package contracts | Public declaration consumers, build, ESM/CommonJS imports, duplicate-dialect coexistence, root isolation, direct-extension bundling, and retained D-041 fallback fixtures pass | | Native/fallback kernel contract | The same eight cases pass against the packaged fallback and native Observable in Chrome `150.0.7871.126` | | Representative bundle delta | `import 'rxjs/map'` falls from 15,726 to 14,447 minified bytes (-1,279; -8.1%), 4,584 to 4,244 gzip bytes (-340; -7.4%), and 4,126 to 3,819 Brotli bytes (-307; -7.4%) | | Root-only bundle control | `import 'rxjs'` is byte-identical before and after: 19,650 minified, 5,638 gzip, and 5,050 Brotli bytes | The bundle comparison used esbuild 0.19.11 for a browser-platform ESM bundle with tree shaking and minification, gzip level 9, and default Brotli settings. The repository and published packages require Node `>=22.13.0`. Node 22 and Node 24 are blocking release lanes, while Node 26 is advisory during beta. The blocking Observable WPT workflow uses Node 24, and the harness unit, import-verification, doctor, and browser-baseline checks have been verified on Node `24.12.0`. D-053 records the final published-package matrix. The P6.2 release baseline was verified on 2026-08-01 and supersedes the package, runtime, bundler, performance, and conformance portions of earlier baselines: | Check | Current result | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Four-package train | All builds, declaration consumers, ESM imports, Node `require(esm)` bridges, migration-document freshness checks, and publication dry runs pass | | Focused source suites | 51 polyfill, 750 RxJS, 75 test-harness, and 166 migration tests pass | | Node | Runtime and ESM/`require(esm)` import contracts pass on 22.13.0, 24.12.0, and advisory 26.5.0 | | Alternate runtimes | The unchanged package-built ESM passes on Deno 2.8.0 and Bun 1.3.14 | | Browser engines | The eight-case contract passes Chrome 151 (native Observable), Firefox 153 (fallback), and WebKit 26.5 (fallback); branded desktop and Mobile Safari use blocking SafariDriver CI lanes | | Webpack and budgets | Webpack 5.106.2 consumes 19 `dist/esm` modules and emits 17,502 bytes against a 22,000-byte ceiling; Node 24 map and cancellation medians exceed their checked-in floors | | Observable WPT | 52/52 URLs, 525/525 upstream subtests, and 52/52 exact RxJS identity attestations pass in pinned Chrome 150 | The complete ported RxJS 7 corpus remains intentionally nonzero: 39 cold and 22 fallback cases encode accepted lifecycle divergences or unsupported arbitrary-subscribable compatibility. They are retained as ordinary executable evidence without skip or expected-failure inversion and are not a release-gate failure. The package-local release-gate contract and current budgets are in `packages/rxjs/docs/RELEASE_GATES.md`. ## Target architecture invariants These invariants should become automated fitness functions: 1. Importing the fallback never replaces an existing Observable or `EventTarget.when`. 2. Native and fallback test modes run the same RxJS platform-layer operator suite. 3. No RxJS-specific string-named property is added to the platform `Observable` constructor or prototype. 4. Every platform operator in the supported RxJS catalog has a corresponding exported Symbol, without changing the platform's string-named method. 5. Every Symbol extension uses an exact module-owned public key and the approved direct-assignment pattern. 6. Independently evaluated package copies coexist under distinct public Symbol keys without replacing one another; `import` and Node `require(esm)` share one module identity. 7. Every returned platform-layer observable preserves the approved constructor behavior within its initialized realm; transparent cross-realm operation is not implied. 8. Cancellation propagates through the platform signal without leaving active upstream work after the last observer leaves. 9. Intentional producer-per-subscription APIs are explicit in imports and types and cannot be reached accidentally through the platform entry point. 10. Every public package entry builds, type-checks, imports, and executes in each supported environment and module system. 11. Every RxJS 7 migration mapping identifies its behavioral evidence, required source change, and any documented divergence without implying a runtime compatibility product. 12. Standards conformance work records the exact specification and WPT revisions under test. 13. Every WPT result used for fallback assessment proves exact RxJS bundle identity in its execution realm; expectation metadata cannot waive that proof. 14. Architecture changes update the decision log and project documents in the same change. 15. Migration tooling never infers lifecycle intent: a migration begins from a reviewed contract manifest, uses one canonical Skill digest across the installed harness adapters, and passes the applicable mechanical and explicitly qualified agent-outcome gates. ## Initial fitness-function scorecard | Characteristic | Check | Target enforcement | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | | Native-first | Import fallback with a sentinel native constructor and assert identity is unchanged | Unit and package-import tests | | Conformance harness | Observable WPT at `6a009d73f0d315941b90cac13a9523a2a08c631b`, with exact bundle identity attested per URL | Blocking strict `test:wpt` job plus a complete-result baseline diagnostic | | Extension safety | Snapshot string properties; verify each module installs only its exported exact Symbol and leaves platform string methods untouched | Unit tests and CI | | Lifecycle | Multi-observer, ref-count, abort, synchronous reentrancy, error, and teardown-order cases | Shared platform test suite | | Native/fallback parity | Run the same operator cases against both implementations | CI matrix | | Package integrity | Build, type, ESM and Node `require(esm)` import, browser/Webpack bundle, runtime-matrix, and duplicate-copy fixtures | Package and release CI | | Migration evidence | RxJS 7 mappings backed by tests or accepted-divergence records without runtime-emulation claims | Migration review and generated-ledger checks | | Mechanical migration | Deterministic fixtures prove diagnostics, containment, dry-run/write equivalence, idempotence, build, and behavior | Package CI and pre-release gate | | Agent migration | Codex/ChatGPT produces approved completion or safe-stop outcomes for the four representative repositories | Offline verification of committed qualification records and artifacts | ## Known architectural risks | Risk | Impact | Mitigation direction | | ---------------------------------------------------------------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | Living platform proposal changes | Polyfill and operators drift from browsers | Pin revisions, track upstream, and advance deliberately | | Global mutation and load order | Native behavior is replaced or imports fail nondeterministically | D-041's conditional transaction, package fixtures, and P0.4's shared lifecycle contract cover the selected base constructor | | Duplicate packages create different Symbols | Extensions appear missing even though code imported them | D-048 documents distinct public keys; package fixtures prove coexistence and consumers use the Symbol from their module copy | | Prototype patching is restricted | Extensions cannot install in hardened or unusual realms | Keep those realms unclaimed; direct assignment may surface native errors or partial paired installation | | RxJS 7 tests encode different producer-per-subscription behavior | False failures lead contributors to corrupt platform semantics | Classify tests and keep cold evidence distinct from platform claims | | Migration evidence is mistaken for runtime compatibility | Users depend on unsupported RxJS 7 imports or lifecycle behavior | State migration actions and unsupported surfaces without publishing an emulation package | | Mechanical output is mistaken for a complete migration | Lifecycle-sensitive changes pass syntax checks but alter behavior | Require a reviewed contract manifest, characterization evidence, and agent-outcome gates | | Harness adapters or copied Skills drift | Different agents give materially different migration advice | Ship one versioned canonical Skill and verify adapter digest plus smoke scenarios | | Package metadata or exports regress | Builds pass locally but published artifacts are unusable | Keep package build, pack, import, and type fixtures as release gates | | Minimal tests allow semantic regressions | Prototype behavior becomes accidental policy | Add lifecycle and extension-kernel safety rails before expanding operators | | Browser-native Observable leaks into a fallback WPT realm | Results falsely appear to prove the RxJS implementation | Exact reference-and-bundle attestation per URL, unsuppressible report audit, negative controls, and reviewed realm patterns | | WPT/browser downloads make conformance impractical | Contributors skip or inconsistently run the gate | Vendor the small approved test closure and checksum-cache the sparse runner, pinned browser, and matching driver | ## Evidence and references Repository evidence: - `packages/observable-polyfill/src/index.ts` - `packages/observable-polyfill/test/import` - `packages/observable-polyfill/test/wpt/config.json` - `packages/observable-polyfill/test/wpt/provenance.json` - `packages/observable-polyfill/test/wpt/expected-test-urls.json` - `packages/rxjs/src/create.ts` - `packages/rxjs/src/index.ts` - `packages/rxjs/test/import` - `packages/rxjs/src/pipe.ts` - `packages/rxjs/src/cold-observable.ts` - `packages/rxjs/src/per-subscription-subject-base.ts` - package manifests and branch history from `origin/master...platform-observable` External sources: - [Observable specification](https://wicg.github.io/observable/) - [Observable proposal and explainer](https://github.com/WICG/observable) - [Observable WPT results](https://wpt.fyi/results/dom/observable/tentative?label=experimental&label=master&aligned) - [Observable WPT sources](https://github.com/web-platform-tests/wpt/tree/master/dom/observable/tentative) - [`rxjs-for-await` async-iteration strategies at the reviewed revision](https://github.com/benlesh/rxjs-for-await/tree/94f9cf9cb015ac3700dfd1850eb81d36962eb70f)