Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@ RxJS is a library for composing asynchronous and event-based programs with
Observable values. This repository contains the platform-based next generation
of RxJS, planned for release as **RxJS 9**.

> RxJS 9 is prerelease work in development. The planned first beta is
> `9.0.0-beta.0`, but it has not been published to npm yet. The `next` tag still
> points to the earlier RxJS 8 prerelease, while RxJS 7 remains the production
> `latest` line and continues to be maintained.
> RxJS 9 is prerelease software. `9.0.0-beta.0` is available under npm's `next`
> tag, while RxJS 7 remains the production `latest` line and continues to be
> maintained.

## Why RxJS 9? What happened to RxJS 8?

Expand All @@ -44,8 +43,8 @@ product.

## Preview the RxJS 9 API

The following example shows the planned beta API. Until `9.0.0-beta.0` is
published, do not use npm's `next` tag to install RxJS 9.
Install the beta with `npm install rxjs@next`. The following example shows the
RxJS 9 beta API.

```ts
import { ColdObservable } from 'rxjs';
Expand Down Expand Up @@ -84,7 +83,7 @@ Repository-wide design records live in

## Supported environments

The planned beta supports Node 22.13+ and Node 24 as blocking lanes, with Node
The RxJS 9 beta supports Node 22.13+ and Node 24 as blocking lanes, with Node
26 in an advisory lane. Current Chrome, Firefox, desktop Safari, Mobile Safari,
Deno, Bun, and Webpack 5 are blocking. Every supported consumer receives the
same ESM implementation, so Deno and Bun support adds no runtime-specific
Expand Down
12 changes: 12 additions & 0 deletions docs/RELEASE_PROCESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ The command performs these steps in order:
8. compares each registry integrity with the local tarball;
9. verifies every package's `next` tag and confirms `rxjs@latest` remains RxJS 7.

For a newly created package, npm may accept the immutable version before its
ordinary package metadata stops returning the pre-publication 404. The command
revalidates online and waits up to ten minutes for that metadata to expose the
published SHA-512 instead of treating normal registry propagation as an
integrity failure.

npm may request OTP/WebAuthn once per package. That repetition is deliberate:
the four packages are independent registry publications. Nothing attempts to
bypass npm's proof-of-presence requirement.
Expand All @@ -68,6 +74,12 @@ freshly packed tarball; a mismatch stops the release. Because `rxjs` is last,
the main consumer entry remains unpublished until the three supporting packages
are present and verified.

If a successful first publication is followed by a metadata 404, do not attempt
an immediate duplicate publish. Wait until `npm view <name>@<version>
dist.integrity --prefer-online` exposes the expected SHA-512, then rerun the
same command. The post-publication retry makes this manual recovery unnecessary
for later betas, but the rule remains safe for an interrupted older client.

If a package was published correctly but a later package cannot be published,
fix only the operational problem and rerun the same command from the same clean
commit. If any source or package byte must change, bump to a fresh beta version.
Expand Down
5 changes: 4 additions & 1 deletion docs/rxjs-next/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,10 @@ 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.
command reports success. npm may accept a brand-new immutable version before
its ordinary package metadata has replaced a cached pre-publication 404, so
post-publication verification revalidates online and retries that read path for
up to ten minutes.

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
Expand Down
6 changes: 4 additions & 2 deletions docs/rxjs-next/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1482,8 +1482,10 @@ Status meanings:
been initialized.
- **Recovery and verification:** A rerun skips an already-published package only
when npm's registry integrity matches the freshly packed tarball. Any byte
mismatch stops. Success requires all four `next` tags to resolve to the exact
version while `rxjs@latest` remains on RxJS 7.
mismatch stops. After a successful publish, the command revalidates online
and waits for npm's eventually consistent package metadata to expose the
immutable version and SHA-512. Success requires all four `next` tags to
resolve to the exact version while `rxjs@latest` remains on RxJS 7.
- **Tradeoff:** The process trusts the maintainer's local machine and npm
authentication and does not provide private staging or automatic npm
provenance. That explicit, understandable boundary is accepted over a more
Expand Down
56 changes: 50 additions & 6 deletions docs/rxjs-next/PROJECT_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,10 @@ wrappers in favor of direct D-037 `[create]` calls and recorded a further
bundle-size reduction. P6.8 completed durable pull-request and `master` CI
ownership for every accepted RxJS 9 test and release check. P6.9 implemented
truthful status signals and security automation and validated their first live
GitHub results. The user has now prioritized P6.10: one understandable,
interactive single-maintainer beta publication command with npm two-factor
authentication and no CI publishing credential.
GitHub results. P6.10 then delivered one understandable, interactive
single-maintainer publication command with npm two-factor authentication and no
CI publishing credential, and used it to publish and verify the first RxJS 9
beta. The planned execution queue is complete.

RxJS 9 and `9.0.0-beta.0` are selected under D-007. D-053 defines runtime,
browser, bundler, channel, and RxJS 7 maintenance policy. Dates and staffing
Expand Down Expand Up @@ -1568,7 +1569,7 @@ names.
| `DONE` | P6.7 | Use direct `[create]` construction and record bundle-size evidence |
| `DONE` | P6.8 | Complete RxJS 9 CI coverage and validate the resulting pull-request workflow matrix |
| `DONE` | P6.9 | Validate the first live dependency-review and Scorecard runs on GitHub |
| `NEXT` | P6.10 | Publish and verify the first beta with the interactive release command |
| `DONE` | P6.10 | Publish and verify the first beta with the interactive release command |

#### P6.9 completion bar

Expand Down Expand Up @@ -1644,8 +1645,22 @@ review`. This completes P6.9; the later P6.10 ruleset migration must preserve
an operator-focused runbook. Existing CI, CodeQL, dependency review, OSV,
package gates, and release-readiness coverage remain.
- Local verification is recorded in the P6.10 session entry. Live npm
OTP/WebAuthn publication, package-access hardening, registry verification,
and the immutable GitHub Release remain required before `DONE`.
OTP/WebAuthn publication completed for all four packages from verified commit
`11d931645c03cc4b67eb70ee3a1122a53f8e0bc9`. The exact registry SHA-512 values
match the rehearsed tarballs, all four `next` tags resolve to
`9.0.0-beta.0`, and `rxjs@latest` remains `7.8.2`.
- npm accepted each new scoped package before its ordinary package metadata
replaced a cached pre-publication 404. Duplicate publication attempts were
rejected immutably. The command now revalidates online and waits up to ten
minutes for post-publication metadata instead of reporting that propagation
interval as an integrity failure.
- `npm access set mfa=publish` completed through browser authentication for
`@rxjs/observable-polyfill`, `@rxjs/test`, `@rxjs/migrate`, and `rxjs`,
requiring interactive 2FA and disallowing automation-token publication.
- Annotated tag `9.0.0-beta.0` targets the verified commit. The public
prerelease is
`https://github.com/ReactiveX/rxjs/releases/tag/9.0.0-beta.0`. P6.10 is
complete and the completed execution queue has no `NEXT` item.

#### P6.1 completion bar

Expand Down Expand Up @@ -3680,3 +3695,32 @@ conformance implementation depends on a runnable harness.
remains the sole `NEXT` item until the command publishes and verifies
`9.0.0-beta.0`, package access disallows automation tokens, and the immutable
GitHub Release is recorded.

### 2026-08-04 — P6.10 first RxJS 9 beta publication

- Published `@rxjs/observable-polyfill`, `@rxjs/test`, `@rxjs/migrate`, and
`rxjs` at `9.0.0-beta.0` under npm `next` from verified commit
`11d931645c03cc4b67eb70ee3a1122a53f8e0bc9`. Master qualification run
`30940527701` and its browser, Safari, Deno, Bun, package, WPT, OSV, CodeQL,
and TypeScript companion workflows passed before publication.
- Verified registry SHA-512 values:
`@rxjs/observable-polyfill` =
`sha512-G2o17zDOxYgCPzM8+//WlDdViovC7NNkGBtlN57YwcVdbt9eMY/UkN1jJWC5j0s+f8Zuiqog1Mf5wFkef8wuAA==`;
`@rxjs/test` =
`sha512-wLBKwkYVHeMLqyJkdz9Yfcex+2/K9lvidOmtrkWWeCZTT+uwNdSgxKlkYbUTFFGjE8PhJsZtCZUmbgBp3tDXQA==`;
`@rxjs/migrate` =
`sha512-a7qOj3mPMOaif0CKVONDvciX6KCe7Pi7TdX/VUgxhLq62le6KikTB+d3n2Ei1pYEU/ngqZj2JIMu/90pZxvgQw==`;
`rxjs` =
`sha512-X7Bpak4JzO/cLamwHfxhtlFtQHpfIjvowcyNErrvHywcdlI+krkbNJc0vQsXKbVOTLrBipCJcIhuawhwsJfWwg==`.
- npm's new-package write path became visible before its ordinary metadata read
path, causing safe partial exits and immutable duplicate rejections during
recovery. Added online revalidation, ETARGET handling, and a bounded
post-publication wait so later betas tolerate the observed propagation lag
without weakening exact-integrity verification.
- Confirmed `rxjs@next` resolves to `9.0.0-beta.0` and `rxjs@latest` remains
`7.8.2`. Applied interactive-only `mfa=publish` access to all four packages.
Created annotated tag `9.0.0-beta.0` and published
`https://github.com/ReactiveX/rxjs/releases/tag/9.0.0-beta.0` as a GitHub
prerelease.
- Marked P6.10 `DONE`. All planned phases are complete, so the execution queue
intentionally has no `NEXT` item.
2 changes: 2 additions & 0 deletions packages/rxjs/docs/SECURITY_ASSURANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ free of vulnerabilities.
packages and confirms that `rxjs@latest` remains on RxJS 7.
- An interrupted command can skip an already-published package only when the
registry integrity equals the freshly packed tarball.
- Post-publication verification revalidates online and tolerates npm metadata
propagation without weakening the exact SHA-512 comparison.

## One-maintainer reality

Expand Down
35 changes: 32 additions & 3 deletions scripts/release/beta.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { mkdtemp, readFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { createInterface } from 'node:readline/promises';
import { setTimeout as delay } from 'node:timers/promises';
import { fileURLToPath, pathToFileURL } from 'node:url';

const repositoryRoot = fileURLToPath(new URL('../..', import.meta.url));
Expand Down Expand Up @@ -90,7 +91,11 @@ export async function main(argv, options = {}) {

process.stdout.write(`\nPublishing ${candidate.name}@${version}. npm may request OTP/WebAuthn.\n`);
interactiveCommand('npm', publishArguments(candidate.tarballPath, { cache: npmCache }), { cwd: root });
const publishedIntegrity = registryIntegrity(candidate.name, version, { cache: npmCache, command, root });
const publishedIntegrity = await waitForRegistryIntegrity(candidate.name, version, {
cache: npmCache,
command,
root,
});
assert.equal(
publishedIntegrity,
candidate.integrity,
Expand Down Expand Up @@ -191,18 +196,42 @@ export function publishArguments(tarballPath, { cache, dryRun = false } = {}) {
function registryIntegrity(name, version, options) {
const result = npmViewResult(`${name}@${version}`, 'dist.integrity', options);
if (result.status === 0) return parseNpmView(result.stdout);
if (/E404|404 Not Found|is not in this registry/i.test(`${result.stdout}\n${result.stderr}`)) return null;
if (
/E404|ETARGET|404 Not Found|is not in this registry|No matching version found|version not found/i.test(
`${result.stdout}\n${result.stderr}`
)
) {
return null;
}
throw new Error(`Could not determine whether ${name}@${version} already exists.\n${result.stdout}${result.stderr}`);
}

export async function waitForRegistryIntegrity(
name,
version,
options,
{ attempts = 121, retryDelayMs = 5_000, wait = delay, output = process.stdout } = {}
) {
for (let attempt = 1; attempt <= attempts; attempt++) {
const integrity = registryIntegrity(name, version, options);
if (integrity !== null) return integrity;
if (attempt === attempts) return null;
if (attempt === 1 || attempt % 6 === 0) {
output.write(`Waiting for npm metadata to expose ${name}@${version} (${attempt}/${attempts})...\n`);
}
await wait(retryDelayMs);
}
return null;
}

function npmView(specifier, field, options) {
const result = npmViewResult(specifier, field, options);
if (result.status !== 0) throw new Error(`npm view ${specifier} ${field} failed.\n${result.stdout}${result.stderr}`);
return parseNpmView(result.stdout);
}

function npmViewResult(specifier, field, { cache, command, root }) {
return command('npm', ['view', specifier, field, '--json', ...(cache ? ['--cache', cache] : [])], {
return command('npm', ['view', specifier, field, '--json', '--prefer-online', ...(cache ? ['--cache', cache] : [])], {
cwd: root,
encoding: 'utf8',
allowFailure: true,
Expand Down
41 changes: 41 additions & 0 deletions scripts/release/beta.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
publishArguments,
releasePackages,
validateBetaVersion,
waitForRegistryIntegrity,
} from './beta.mjs';

test('accepts one explicit RxJS 9 beta and an optional dry run', () => {
Expand Down Expand Up @@ -109,3 +110,43 @@ test('live publication refuses CI, environment tokens, and non-interactive termi
assertInteractivePublishingEnvironment({ dryRun: true, env: { CI: 'true', NPM_TOKEN: 'secret' }, stdin: {}, stdout: {} })
);
});

test('waits for npm metadata propagation after a successful publication', async () => {
let lookups = 0;
const waits = [];
const output = [];
const command = (_command, args) => {
assert.ok(args.includes('--prefer-online'));
lookups++;
if (lookups < 3) return { status: 1, stdout: '', stderr: 'npm error code E404' };
return { status: 0, stdout: '"sha512-published"\n', stderr: '' };
};

const integrity = await waitForRegistryIntegrity(
'@rxjs/new-package',
'9.0.0-beta.0',
{ cache: '/tmp/cache', command, root: '/repo' },
{
attempts: 3,
retryDelayMs: 10,
wait: async (milliseconds) => waits.push(milliseconds),
output: { write: (message) => output.push(message) },
}
);

assert.equal(integrity, 'sha512-published');
assert.equal(lookups, 3);
assert.deepEqual(waits, [10, 10]);
assert.match(output.join(''), /Waiting for npm metadata/);
});

test('treats an existing package with an unpropagated version as temporarily missing', async () => {
const command = () => ({ status: 1, stdout: '', stderr: 'npm error code ETARGET\nNo matching version found' });
const integrity = await waitForRegistryIntegrity(
'rxjs',
'9.0.0-beta.1',
{ cache: '/tmp/cache', command, root: '/repo' },
{ attempts: 1, wait: async () => {}, output: { write: () => {} } }
);
assert.equal(integrity, null);
});
Loading