diff --git a/.cursor/BUGBOT.md b/.cursor/BUGBOT.md new file mode 100644 index 0000000000..99672195dc --- /dev/null +++ b/.cursor/BUGBOT.md @@ -0,0 +1,45 @@ +# RxJS Bugbot Review Rules + +Project-specific PR review instructions for the RxJS 8 monorepo. Default branch is `master`. Use **Yarn 1.22.21** — not npm workspaces. + +Reference docs (follow these; do not duplicate in review comments): + +- [AGENTS.md](../AGENTS.md) — package map and commands +- [CONTRIBUTING.md](../CONTRIBUTING.md) — PR flow, style, commits, tests +- [.cursor/rules/](./rules/) — scoped authoring rules for agents + +## Repository-wide gates + +If the PR modifies files under `.nx/cache/**`, `.yarn/cache/**`, `.tshy/**`, `dist/**`, or `**/generated/**` (except intentional release artifacts committed by maintainers), then add a blocking bug titled **"Do not commit build or cache output"** with body: "Revert generated, cache, and dist edits. Edit source files instead." + +If the PR modifies `package-lock.json` or uses npm-specific workspace config instead of the repo's Yarn 1 setup, then add a blocking bug titled **"Use Yarn 1.22.21"** with body: "This monorepo uses Yarn 1.22.21. Do not add npm lockfiles or npm workspace changes." + +If the PR targets a base branch other than `master` without clear release-maintainer context, then add a blocking bug titled **"Target master by default"** with body: "RxJS contribution PRs should target `ReactiveX/rxjs:master` unless a maintainer is intentionally preparing a release or backport branch." + +If the PR adds pipeable operators or operator-like transforms under `packages/observable/**`, then add a blocking bug titled **"Operators belong in packages/rxjs"** with body: "`@rxjs/observable` is core primitives only. Move operator logic to `packages/rxjs/src/internal/operators/`." + +If the PR edits API documentation output under `apps/rxjs.dev/` that is generated from JSDoc (dgeni output), then add a blocking bug titled **"Do not edit generated API docs"** with body: "Update JSDoc in `packages/rxjs/src/` and regenerate with `yarn workspace rxjs.dev docs`. Hand-written guides live in `apps/rxjs.dev/content/`." + +If the PR changes public exports in `packages/rxjs/src/index.ts` or subpath barrels (`src/ajax/`, `src/fetch/`, `src/testing/`, `src/webSocket/`) without corresponding tests or JSDoc updates in the changed API surface, then add a blocking bug titled **"Public API change missing tests or docs"** with body: "New or changed public API requires marble or unit tests, dtslint when types change, and JSDoc with `@example` (marble PNG when stream-shaped)." + +If a non-trivial PR has an empty or vague PR description that does not explain the problem, the chosen fix, and how it was tested, then add a non-blocking bug titled **"PR description missing review context"** with body: "Maintainers repeatedly ask for enough context to judge necessity and scope. Summarize the problem, implementation approach, and test/CI coverage in the PR body." + +If the PR adds a new public API surface (operator, creation function, scheduler, subject helper, exported type, or package subpath) without linking an issue, discussion, or clear rationale for why it belongs in core rather than userland, then add a blocking bug titled **"New public API needs core rationale"** with body: "New RxJS API needs explicit maintainer-facing rationale: use case, semantics, naming, compatibility, and why this should live in RxJS core instead of a userland package." + +If a PR mixes material library behavior changes with broad formatting-only or whitespace-only edits across unrelated files, then add a non-blocking bug titled **"Split formatting churn from behavior changes"** with body: "Keep mechanical formatting or move-only churn out of behavior PRs so reviewers can see the semantic diff. Put broad formatting changes in a follow-up PR." + +If changed Markdown, HTML, or JSDoc adds links to paths that do not exist in this repository, references old repo locations such as `docs_app/`, or uses stale version names that conflict with the current RxJS 8 branch, then add a blocking bug titled **"Docs link or version drift"** with body: "Update links and version references to the current monorepo layout. Do not introduce stale `docs_app/` paths or outdated RxJS version guidance." + +If the PR changes GitHub Actions workflows, package scripts, TypeScript config, dependency installation, or lockfile behavior in a way that drops the Node 18/20 CI matrix, skips `rxjs` lint/build/test/dtslint/import/esm checks, or bypasses `rxjs.dev` build/test checks without an explicit maintainer migration note, then add a blocking bug titled **"Preserve RxJS CI coverage"** with body: "CI changes must keep the package and docs checks that catch contribution regressions: Node 18/20, lint, build, unit tests, dtslint, import/esm tests, and rxjs.dev build/test unless a maintainer is intentionally migrating CI." + +## Non-blocking guidance + +If the only issues are Prettier/ESLint formatting that CI auto-fixes, do not file blocking bugs — mention as a non-blocking note only when the diff clearly violates stated style (single quotes, 140 char width, `import type` preference). + +If the PR is an intentional breaking change for RxJS 8 with migration notes in `apps/rxjs.dev/content/deprecations/`, do not flag removal of deprecated APIs as a regression. + +Scoped rules for changed paths: + +- `packages/rxjs/` → [packages/rxjs/.cursor/BUGBOT.md](../packages/rxjs/.cursor/BUGBOT.md) +- `packages/observable/` → [packages/observable/.cursor/BUGBOT.md](../packages/observable/.cursor/BUGBOT.md) +- `apps/rxjs.dev/` → [apps/rxjs.dev/.cursor/BUGBOT.md](../apps/rxjs.dev/.cursor/BUGBOT.md) diff --git a/.cursor/hooks.json b/.cursor/hooks.json new file mode 100644 index 0000000000..a8c09545fc --- /dev/null +++ b/.cursor/hooks.json @@ -0,0 +1,17 @@ +{ + "version": 1, + "hooks": { + "afterFileEdit": [ + { + "command": ".cursor/hooks/format-and-lint.sh" + } + ], + "beforeShellExecution": [ + { + "command": ".cursor/hooks/before-git-commit.sh", + "matcher": "git\\s+commit", + "failClosed": true + } + ] + } +} diff --git a/.cursor/hooks/before-git-commit.sh b/.cursor/hooks/before-git-commit.sh new file mode 100755 index 0000000000..4e03e0af4b --- /dev/null +++ b/.cursor/hooks/before-git-commit.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +# Inject conventional-commit guidance before agent git commits. +# Validates extracted messages; blocks invalid commits via permission: deny. + +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +VALIDATOR="$ROOT/.cursor/skills/rxjs-conventional-commits/scripts/validate-message.sh" +SKILL=".cursor/skills/rxjs-conventional-commits/SKILL.md" + +input="$(cat)" +command="$(echo "$input" | jq -r '.command // empty')" + +# Only git commit (including --amend); ignore other git subcommands. +if [[ ! "$command" =~ (^|[[:space:]])git[[:space:]]+commit([[:space:]]|$) ]]; then + echo '{ "permission": "allow" }' + exit 0 +fi + +extract_heredoc_message() { + local cmd="$1" + + # Single-line HEREDOC: git commit -m "$(cat <<'EOF' body EOF )" + if [[ "$cmd" == *"<<"*"EOF"* ]]; then + local inline + inline="$(printf '%s' "$cmd" | awk ' + match($0, /<<[[:space:]]*('\''EOF'\''|"EOF"|EOF)[[:space:]]*/) { + start = RSTART + RLENGTH + rest = substr($0, start) + if (match(rest, /EOF/)) { + body = substr(rest, 1, RSTART - 1) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", body) + gsub(/\)[[:space:]]*$/, "", body) + if (length(body) > 0) { + print body + } + } + } + ')" + if [[ -n "$inline" ]]; then + printf '%s' "$inline" + return 0 + fi + fi + + # Multi-line HEREDOC (command string contains literal newlines) + if [[ "$cmd" == *$'\n'* && "$cmd" == *"<<"*"EOF"* ]]; then + local multiline + multiline="$(printf '%s\n' "$cmd" | awk ' + BEGIN { in_body = 0 } + /cat[[:space:]]+<<[[:space:]]*/ { + in_body = 1 + next + } + in_body && (/^EOF[[:space:]]*\)/ || /^EOF[[:space:]]*$/) { + exit + } + in_body { + print + } + ')" + if [[ -n "$multiline" ]]; then + printf '%s' "$multiline" + return 0 + fi + fi + + return 1 +} + +extract_message() { + local cmd="$1" + local msg="" + + if msg="$(extract_heredoc_message "$cmd")"; then + printf '%s' "$msg" + return 0 + fi + + # Repeated -m "..." / -m '...' (git joins with newlines) + while [[ "$cmd" =~ -m[[:space:]]+ ]]; do + local fragment="" + if [[ "$cmd" =~ -m[[:space:]]+\"(([^\"\\]|\\.)*)\" ]]; then + fragment="${BASH_REMATCH[1]}" + cmd="${cmd/-m \"${BASH_REMATCH[1]}\"/}" + elif [[ "$cmd" =~ -m[[:space:]]+\'([^\']*)\' ]]; then + fragment="${BASH_REMATCH[1]}" + cmd="${cmd/-m \'${BASH_REMATCH[1]}\'/}" + else + break + fi + if [[ -n "$msg" ]]; then + msg+=$'\n' + fi + msg+="$fragment" + done + + if [[ -n "$msg" ]]; then + printf '%s' "$msg" + fi +} + +deny_invalid_message() { + local validation="$1" + jq -n \ + --arg skill "$SKILL" \ + --arg validation "$validation" \ + '{ + permission: "deny", + user_message: "Commit message does not follow RxJS conventional-commit format.", + agent_message: ("Rewrite the commit message per " + $skill + " before committing.\n\nValidation errors:\n" + $validation) + }' +} + +message="$(extract_message "$command" || true)" + +if [[ -n "$message" && -x "$VALIDATOR" ]]; then + validation_err="$(mktemp)" + if ! "$VALIDATOR" "$message" 2>"$validation_err"; then + validation="$(cat "$validation_err")" + rm -f "$validation_err" + deny_invalid_message "$validation" + exit 0 + fi + rm -f "$validation_err" +fi + +jq -n \ + --arg skill "$SKILL" \ + '{ + permission: "allow", + agent_message: ("Follow RxJS conventional commits (" + $skill + "): type(scope): subject, imperative lowercase subject, max 100 chars/line. Run git diff first; use a HEREDOC for multi-line messages.") + }' diff --git a/.cursor/hooks/format-and-lint.sh b/.cursor/hooks/format-and-lint.sh new file mode 100755 index 0000000000..aebecca427 --- /dev/null +++ b/.cursor/hooks/format-and-lint.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Post-edit formatter: runs eslint --fix and prettier --write on agent-edited files. +# Always exits 0 so lint/format issues never block the agent. + +set -uo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +ESLINT="$ROOT/node_modules/.bin/eslint" +PRETTIER="$ROOT/node_modules/.bin/prettier" + +input="$(cat)" +file_path="$(echo "$input" | jq -r '.file_path // empty')" + +if [[ -z "$file_path" ]]; then + exit 0 +fi + +# Normalize to an absolute path under the repo when possible. +if [[ "$file_path" != /* ]]; then + file_path="$ROOT/$file_path" +fi + +# Only process files inside this repository. +case "$file_path" in + "$ROOT"/*) ;; + *) exit 0 ;; +esac + +rel="${file_path#"$ROOT"/}" + +# Skip generated, vendored, and non-source paths. +case "$rel" in + node_modules/* | dist/* | .yarn/* | yarn.lock | package-lock.json | *.min.js | *.min.css) + exit 0 + ;; +esac + +run_quietly() { + "$@" >/dev/null 2>&1 || true +} + +case "$rel" in + *.ts | *.tsx | *.js | *.jsx) + if [[ -x "$ESLINT" ]]; then + run_quietly "$ESLINT" --fix "$file_path" + fi + ;; +esac + +case "$rel" in + *.ts | *.tsx | *.js | *.jsx | *.css | *.md | *.json | *.yml | *.yaml) + if [[ -x "$PRETTIER" ]]; then + run_quietly "$PRETTIER" --write "$file_path" + fi + ;; +esac + +exit 0 diff --git a/.cursor/mcp.json b/.cursor/mcp.json new file mode 100644 index 0000000000..428f8ef23d --- /dev/null +++ b/.cursor/mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "firebase": { + "command": "npx", + "args": ["-y", "firebase-tools@latest", "mcp", "--dir", "${workspaceFolder}/apps/rxjs.dev", "--only", "hosting"] + } + } +} diff --git a/.cursor/rules/rxjs-core-internals.mdc b/.cursor/rules/rxjs-core-internals.mdc new file mode 100644 index 0000000000..107aa13464 --- /dev/null +++ b/.cursor/rules/rxjs-core-internals.mdc @@ -0,0 +1,55 @@ +--- +description: Non-operator RxJS internals — observables, subjects, schedulers, ajax, util +globs: packages/rxjs/src/internal/*.ts,packages/rxjs/src/internal/observable/**,packages/rxjs/src/internal/scheduler/**,packages/rxjs/src/internal/ajax/**,packages/rxjs/src/internal/util/**,packages/rxjs/src/internal/scheduled/**,packages/rxjs/src/internal/testing/**,packages/rxjs/src/ajax/**,packages/rxjs/src/fetch/**,packages/rxjs/src/testing/**,packages/rxjs/src/webSocket/** +alwaysApply: false +--- + +# RxJS Core Internals + +Not pipeable operators. For operator patterns, see `rxjs-operators`. + +## Categories + +| Area | Path | Examples | +|------|------|----------| +| Creation functions | `internal/observable/` | `of`, `from`, `defer`, `timer` | +| Subjects | `internal/*Subject.ts` | `BehaviorSubject`, `ReplaySubject` | +| Schedulers | `internal/scheduler/` | `async`, `queue`, `animationFrame` | +| Ajax / WebSocket | `internal/ajax/`, `webSocket/` | HTTP helpers, socket wrapper | +| Utilities | `internal/util/` | errors, type guards, helpers | +| Testing | `internal/testing/`, `src/testing/` | `TestScheduler`, test doubles | + +## Sibling references + +Before inventing patterns, read the nearest existing implementation and spec pair: + +| Area | Implementation | Spec | +|------|----------------|------| +| Creation | `internal/observable/defer.ts`, `timer.ts` | `spec/observables/defer-spec.ts` | +| Schedulers | `internal/scheduler/AsyncScheduler.ts`, `async.ts`, `AsyncAction.ts` | `spec/schedulers/QueueScheduler-spec.ts` | +| Timer limits | `internal/util/maxTimerDelay.ts` (when present), `AsyncAction.schedule` | `spec/observables/timer-spec.ts`, `spec/operators/delay-spec.ts` | +| Subjects | `internal/BehaviorSubject.ts` | `spec/subjects/BehaviorSubject-spec.ts` | +| Ajax | `internal/ajax/ajax.ts` | `spec/ajax/index-spec.ts` | + +Real timer validation for `asyncScheduler` belongs in **`AsyncAction.schedule`** (the `setInterval` choke point), not only in individual operators. + +## Implementation + +- Creation functions return `Observable` directly (not `OperatorFunction`) +- Subjects extend `Subject`; schedulers implement `Scheduler` / `SchedulerAction` +- Use `@rxjs/observable` primitives; keep operator logic out of this layer + +## Exports + +- Main API: `packages/rxjs/src/index.ts` +- Subpath barrels: `src/ajax/`, `src/fetch/`, `src/testing/`, `src/webSocket/` + +## Tests + +- `spec/observables/`, `spec/subjects/`, `spec/schedulers/`, `spec/ajax/`, `spec/websocket/` +- Marble tests where timing matters; synchronous Chai tests where appropriate +- Not the operator-only canonical-case checklist — match patterns in sibling specs + +## JSDoc + +Public API still needs JSDoc with `@example`. Marble PNG diagrams when the API is stream-shaped. diff --git a/.cursor/rules/rxjs-dev-site.mdc b/.cursor/rules/rxjs-dev-site.mdc new file mode 100644 index 0000000000..efc54cf6d3 --- /dev/null +++ b/.cursor/rules/rxjs-dev-site.mdc @@ -0,0 +1,34 @@ +--- +description: rxjs.dev docs site — dgeni API docs vs hand-written guides +globs: apps/rxjs.dev/** +alwaysApply: false +--- + +# rxjs.dev Docs Site + +## Two doc sources + +| Source | Location | How it updates | +|--------|----------|----------------| +| API docs | Generated from JSDoc in `packages/rxjs/src/` | Edit source JSDoc; run `yarn workspace rxjs.dev docs` | +| Guides & deprecations | `apps/rxjs.dev/content/` | Edit markdown directly | + +Never edit generated API JSON output. + +## When changing public API + +Update JSDoc in `packages/rxjs/src/` (informal span, marble PNG, `@example`). Guides in `content/` only when adding narrative docs (e.g. new operator category in `content/guide/operators.md`). + +## App stack + +- Angular 13 docs application +- Component tests: Jasmine/Karma (`yarn workspace rxjs.dev test`) +- Doc generation: dgeni (`yarn workspace rxjs.dev docs`) + +## Dev + +```sh +yarn workspace rxjs.dev start +``` + +Deep reference: [apps/rxjs.dev/README.md](../../apps/rxjs.dev/README.md) diff --git a/.cursor/rules/rxjs-dtslint.mdc b/.cursor/rules/rxjs-dtslint.mdc new file mode 100644 index 0000000000..87ee5914df --- /dev/null +++ b/.cursor/rules/rxjs-dtslint.mdc @@ -0,0 +1,51 @@ +--- +description: Type-level test conventions with dtslint directives +globs: packages/rxjs/spec-dtslint/** +alwaysApply: false +--- + +# dtslint Type Tests + +## File layout + +Mirror operator/API name: `spec-dtslint/operators/-spec.ts` + +## Patterns + +```typescript +import type { Observable } from 'rxjs'; +import { of } from 'rxjs'; +import { filter } from 'rxjs/operators'; + +it('should support a predicate', () => { + const o = of(1, 2, 3).pipe(filter((value) => value < 3)); // $ExpectType Observable +}); + +it('should enforce types', () => { + const o = of(1, 2, 3).pipe(filter()); // $ExpectError +}); + +it('should deprecate thisArg usage', () => { + const a = of(1, 2, 3).pipe(filter(Boolean)); // $ExpectNoDeprecation + const b = of(1, 2, 3).pipe(filter(Boolean, {})); // $ExpectDeprecation +}); +``` + +Directives are **inline end-of-line comments** on the expression they assert: + +- `// $ExpectType ` — inferred type +- `// $ExpectError` — type error on that line +- `// $ExpectDeprecation` / `// $ExpectNoDeprecation` — deprecation diagnostics + +## When to add + +- New operators: type test at end of marble spec **and** dedicated dtslint file when signatures are non-trivial +- Cover overloads, type guards, deprecations, and invalid argument combinations + +## Run + +```sh +yarn workspace rxjs dtslint +``` + +Runs ESLint + `tsc -b` on type definitions. diff --git a/.cursor/rules/rxjs-marble-tests.mdc b/.cursor/rules/rxjs-marble-tests.mdc new file mode 100644 index 0000000000..9e31a5cc7e --- /dev/null +++ b/.cursor/rules/rxjs-marble-tests.mdc @@ -0,0 +1,55 @@ +--- +description: Operator marble test patterns and required test cases +globs: packages/rxjs/spec/operators/** +alwaysApply: false +--- + +# Operator Marble Tests + +For CI vs local gaps, GC leak tests, and non-marble Mocha specs, see `rxjs-mocha-specs`. + +## Framework + +Mocha + Chai. Async operators use `TestScheduler.run()` — not manual scheduler setup unless necessary. + +## Setup + +```typescript +beforeEach(() => { testScheduler = new TestScheduler(observableMatcher); }); + +testScheduler.run(({ cold, expectObservable, expectSubscriptions }) => { + const e1 = cold(' --1--2--3--|'); + expectObservable(e1.pipe(map((x) => 10 * +x))).toBe('--x--y--z--|', { x: 10, y: 20, z: 30 }); + expectSubscriptions(e1.subscriptions).toBe('^----------!'); +}); +``` + +## Naming + +- File: `spec/operators/-spec.ts` +- JSDoc on describe: `/** @test {map} */` + +## Required cases per operator + +- never, empty, single, multiple, error, never-ending, early disposal +- If operator takes a callback: success, context (if supported), thrown error + +## Assertions + +For marble tests with cold/hot source observables, assert subscriptions unless the source has no meaningful subscription log. + +## Real timers inside marbles + +Default marbles use **virtual time** (`TestScheduler`). Bugs involving `setTimeout` / `setInterval` platform limits need **real** scheduling: + +- Keep `TestScheduler.run()` for structure, `expectObservable`, and `expectSubscriptions` +- Pass **`asyncScheduler`** explicitly to the operator or creation function under test +- Assert synchronous errors with `#` and the expected error as the **third** `toBe` argument (see `elementAt-spec.ts`, `delay-spec.ts`, `timer-spec.ts`) + +```typescript +expectObservable(source).toBe('#', null, expectedRangeError); +``` + +Do not replace marbles with bare `subscribe({ error })` unless the bug is specifically about unhandled async errors. + +Deep reference: [marble-testing.md](../../apps/rxjs.dev/content/guide/testing/marble-testing.md) diff --git a/.cursor/rules/rxjs-mocha-specs.mdc b/.cursor/rules/rxjs-mocha-specs.mdc new file mode 100644 index 0000000000..20ee33dd7a --- /dev/null +++ b/.cursor/rules/rxjs-mocha-specs.mdc @@ -0,0 +1,90 @@ +--- +description: RxJS Mocha spec patterns — CI vs local, GC leak tests, non-marble specs +globs: packages/rxjs/spec/** +alwaysApply: false +--- + +# RxJS Mocha Specs + +Mocha + Chai under `packages/rxjs/spec/`. Operator marbles → `rxjs-marble-tests` rule. + +## CI vs local (important) + +GitHub Actions runs **Node 18 and Node 20**. `yarn workspace rxjs test` locally often **passes while CI Node 20 fails** because: + +| Condition | Local (typical) | CI Node 18 | CI Node 20 | +| --------- | --------------- | ---------- | ---------- | +| `global.gc` available | Often **no** | **No** | **Yes** | +| GC leak tests run | Skipped | Skipped | **Run** | + +GC leak tests gate on `FinalizationRegistry && global.gc`. When skipped, specs log `No support for FinalizationRegistry in Node …` (misleading — usually missing `global.gc`). + +`.mocharc.js` sets `'expose-gc': true`, but **Mocha 10 ignores it**. Use `node --expose-gc` to match CI Node 20 locally: + +```sh +cd packages/rxjs +node --expose-gc ./node_modules/.bin/mocha \ + --config spec/support/.mocharc.js "spec/**/*-spec.ts" +``` + +Before opening a PR, prefer the full CI-style check: + +```sh +yarn nx run-many -t build lint test --exclude=rxjs.dev +``` + +Nx/build on agents needs unrestricted permissions (daemon socket); sandboxed runs can hang. + +## GC leak tests (`FinalizationRegistry`) + +Only two specs use this pattern today: + +- `spec/Subscriber-spec.ts` — `should not leak the destination` +- `spec/operators/shareReplay-spec.ts` — `should not leak the subscriber for sync sources` + +Pattern: + +```typescript +const FinalizationRegistry = (global as any).FinalizationRegistry; +if (FinalizationRegistry && global.gc) { + it('should not leak …', (done) => { + let target: SomeType | undefined = …; + const registry = new FinalizationRegistry(() => { + done(); + }); + registry.register(target, 'held-value'); + // exercise API — do NOT keep handles that retain `target` + of(42).subscribe(target as any); + target = undefined; + global.gc?.(); + }); +} +``` + +**Do not** assign `const subscription = source.subscribe(target)` unless the test is about that subscription — a closed `Subscriber` still holds `destination` and prevents collection. + +Do not add new GC leak tests unless necessary; they are flaky by nature and slow CI debugging. + +## File layout + +| Area | Path | +| ---- | ---- | +| Operators (marbles) | `spec/operators/-spec.ts` | +| Observables | `spec/observables/` | +| Subjects, schedulers | `spec/subjects/`, `spec/schedulers/` | +| Core | `spec/Subscriber-spec.ts`, `spec/Observable-spec.ts`, … | + +Setup: `spec/support/.mocharc.js` (5s default timeout, `spec/helpers/setup.ts`). + +## `src` vs `dist` in specs + +- `import { … } from 'rxjs'` → `tsconfig` paths → **`src`** (preferred in specs) +- `import … from 'rxjs/internal/…'` → package **exports** → **`dist/cjs`** (can be **stale** until built) + +After changing `internal/scheduler/` or `internal/util/`, run `yarn workspace rxjs build` before debugging confusing test failures. + +## Timer limit pitfalls + +- Platform `setTimeout` / `setInterval` max delay: `2^31 - 1` ms (`2147483647`, ~24.8 days) +- Do **not** `sinon.useFakeTimers().tick(2147483647)` — overflows or hangs +- For oversize-delay bugs: marble test + explicit `asyncScheduler` (see `rxjs-marble-tests`) diff --git a/.cursor/rules/rxjs-observable-core.mdc b/.cursor/rules/rxjs-observable-core.mdc new file mode 100644 index 0000000000..f2e78c7c34 --- /dev/null +++ b/.cursor/rules/rxjs-observable-core.mdc @@ -0,0 +1,37 @@ +--- +description: @rxjs/observable package — Vitest, tshy build, dependency boundaries +globs: packages/observable/** +alwaysApply: false +--- + +# @rxjs/observable + +## Scope + +Core primitives only: `Observable`, `Subscriber`, `Subscription`, `operate()`. No operators. + +## Tests + +Vitest — not Mocha marbles: + +```typescript +import { describe, expect, it, vi } from 'vitest'; +``` + +Spec files: `packages/observable/src/*.spec.ts` + +## Build + +`tshy` for dual CJS/ESM output (not `tsc -b` like `packages/rxjs`). + +## Dependency direction + +- `packages/rxjs` depends on `@rxjs/observable` +- Do **not** import rxjs operators or schedulers from this package + +## Commands + +```sh +yarn nx test @rxjs/observable +yarn nx build @rxjs/observable +``` diff --git a/.cursor/rules/rxjs-operators.mdc b/.cursor/rules/rxjs-operators.mdc new file mode 100644 index 0000000000..1cf82dd8e4 --- /dev/null +++ b/.cursor/rules/rxjs-operators.mdc @@ -0,0 +1,42 @@ +--- +description: Operator file layout, exports, JSDoc, and PR checklist +globs: packages/rxjs/src/internal/operators/**,packages/rxjs/src/operators/** +alwaysApply: false +--- + +# RxJS Operators + +## File layout + +- One operator per file: `packages/rxjs/src/internal/operators/.ts` +- camelCase filename matching export (`map.ts`, `distinctUntilChanged.ts`) + +## Exports + +Add the operator to both: + +1. `packages/rxjs/src/operators/index.ts` +2. `packages/rxjs/src/index.ts` + +## Implementation + +Pipeable operator returning `(source) => new Observable(...)` using `operate()` from `@rxjs/observable`. See `packages/rxjs/src/internal/operators/map.ts`. + +## JSDoc (required for public API) + +- Informal `` summary +- `![](.png)` marble diagram image alongside operator file +- `@see`, `@param`, `@return` +- Runnable `@example` block with imports + +## Docs checklist (new public operators) + +Applies when shipping a new **public** operator — not internal refactors or deprecated-adjacent tweaks. + +- [ ] `-spec.ts` with marble tests for canonical cases +- [ ] Type definition test (spec end + `spec-dtslint/` when non-trivial) +- [ ] JSDoc + PNG marble diagram +- [ ] Listed in `apps/rxjs.dev/content/guide/operators.md` (public operators only) +- [ ] Deprecation or breaking changes documented in `apps/rxjs.dev/content/deprecations/` when applicable + +Deep reference: [operators.md § Creating custom operators](../../apps/rxjs.dev/content/guide/operators.md#creating-custom-operators) diff --git a/.cursor/rules/rxjs-repo.mdc b/.cursor/rules/rxjs-repo.mdc new file mode 100644 index 0000000000..06d745f645 --- /dev/null +++ b/.cursor/rules/rxjs-repo.mdc @@ -0,0 +1,36 @@ +--- +description: RxJS monorepo boundaries, tooling, and style defaults +alwaysApply: true +--- + +# RxJS Monorepo + +Canonical package map, commands, skills, and human docs: [AGENTS.md](../../AGENTS.md). + +## Where to put changes + +- Library code: `packages/rxjs` (operators, schedulers, subjects) or `packages/observable` (core Observable primitives) +- Hand-written docs: `apps/rxjs.dev/content/` +- API docs: JSDoc in `packages/rxjs/src/` (generated by dgeni — never edit build output) +- Tests: `packages/rxjs/spec/` (Mocha marbles), `packages/rxjs/spec-dtslint/` (types), `packages/observable/src/*.spec.ts` (Vitest) + +## Package boundaries + +- `packages/rxjs` depends on `@rxjs/observable`; operators use `new Observable` and `operate()` from that package +- Do not put operators in `packages/observable` + +## Style + +- 2-space indent, single quotes, 140 char line width (Prettier) +- Prefer `import type` / `export type` (ESLint warn) +- ESM imports in `packages/rxjs` use `.js` extensions: `'../types.js'` + +## Scoped rules + +- Operators → `rxjs-operators` +- Core internals (observables, subjects, schedulers) → `rxjs-core-internals` +- Operator marble tests → `rxjs-marble-tests` +- Mocha specs (CI/GC parity) → `rxjs-mocha-specs` +- Type tests → `rxjs-dtslint` +- `@rxjs/observable` → `rxjs-observable-core` +- Docs site → `rxjs-dev-site` diff --git a/.cursor/skills/rxjs-bug-fix/SKILL.md b/.cursor/skills/rxjs-bug-fix/SKILL.md new file mode 100644 index 0000000000..258e6d7927 --- /dev/null +++ b/.cursor/skills/rxjs-bug-fix/SKILL.md @@ -0,0 +1,63 @@ +--- +name: rxjs-bug-fix +description: >- + Fix reported RxJS operator, scheduler, or observable bugs end-to-end: triage, + failing marble test, implementation, JSDoc, and verification. Use when fixing + a GitHub issue, regression, or unexpected timer/scheduler behavior. +--- + +# RxJS Bug Fix + +End-to-end workflow for **reported bugs** in `packages/rxjs`. Complements `rxjs-repro-to-test` (test-only) and `rxjs-github-issue` (issue triage). + +## Before you write code + +1. **Triage the issue** — run `rxjs-github-issue` steps or `gh issue view --repo ReactiveX/rxjs` +2. **Classify timing** — see scheduler triage in `rxjs-repro-to-test` (virtual time vs real `asyncScheduler`) +3. **Write or confirm a failing test** — marble first; see `rxjs-marble-tests` and `rxjs-repro-to-test` +4. **Trace the call chain** before picking a fix location: + + | User API | Typical chain | + | ------------------------------- | ----------------------------------------------------------------------------------------- | + | `delay` / `delayWhen` | operator → `timer` → `executeSchedule` → `AsyncScheduler` → `AsyncAction` → `setInterval` | + | `timer` / `interval` | creation → scheduler → `AsyncAction` | + | `debounceTime`, `bufferTime`, … | operator → `executeSchedule` → scheduler | + + Real `setTimeout` / `setInterval` limits are enforced in **`AsyncAction.schedule`** (via `maxTimerDelay.ts` when present), not only in operators. + +## Fix-style decision + +| Approach | When | +| ---------------------- | ----------------------------------------------------------------- | +| **Throw `RangeError`** | Invalid input; silent wrong behavior is worse than failing fast | +| **Chunk / reschedule** | API contract requires delays beyond platform limits to still work | +| **Document only** | WONTFIX; user must precompute or use a different scheduler | + +Discuss with the reporter when the issue comment already identifies root cause (e.g. `setTimeout` max ≈ 24.8 days). + +## Implementation checklist + +1. **Minimal fix** at the lowest shared layer (prefer scheduler/util over duplicating per operator) +2. **JSDoc** — update “Known limitations” on public APIs (`timer`, operators that delegate to it) +3. **Marble tests** — `spec/operators/-spec.ts` and/or `spec/observables/-spec.ts` +4. **Build** — `yarn workspace rxjs build` when debugging tests that resolve `rxjs/internal/*` to `dist/` +5. **Verify** + + ```sh + yarn workspace rxjs test -- --grep '' + yarn nx run-many -t build lint test --exclude=rxjs.dev + ``` + +## Do not + +- Fix implementation in the same step as `rxjs-repro-to-test` unless the user asked for a full fix +- Use Vitest in `packages/rxjs` — Mocha + marbles only +- Import `rxjs/internal/*` in specs — use `rxjs` or `../../src/...` (see `rxjs-mocha-specs`) +- Tick fake timers by `2147483647` ms (`2^31 - 1`) — overflows / hangs + +## Reference + +- `rxjs-repro-to-test` — failing marble from a repro +- `rxjs-github-issue` — GitHub issue intake +- `rxjs-core-internals` — schedulers, creation functions, util layout +- `rxjs-conventional-commits` — commit message format diff --git a/.cursor/skills/rxjs-conventional-commits/SKILL.md b/.cursor/skills/rxjs-conventional-commits/SKILL.md new file mode 100644 index 0000000000..78bb022d3b --- /dev/null +++ b/.cursor/skills/rxjs-conventional-commits/SKILL.md @@ -0,0 +1,122 @@ +--- +name: rxjs-conventional-commits +description: >- + Format git commit messages for the RxJS monorepo using conventional commits + from CONTRIBUTING.md. Use when writing commit messages, running git commit, + staging changes for commit, or when the user asks to commit. +--- + +# RxJS Conventional Commits + +RxJS generates **release changelogs from commit messages**. Every commit must follow the format in [CONTRIBUTING.md](../../../CONTRIBUTING.md#commit-message-format). + +## Before committing + +1. Run `git status` and `git diff` (staged + unstaged) to understand the change. +2. Pick **one logical change** per commit; split unrelated edits. +3. Draft the message using the rules below. +4. Validate (optional but recommended): + + ```sh + .cursor/skills/rxjs-conventional-commits/scripts/validate-message.sh <<'EOF' + type(scope): subject + + body paragraph + EOF + ``` + +5. Commit with a HEREDOC so formatting is preserved: + + ```sh + git commit -m "$(cat <<'EOF' + type(scope): subject + + Optional body explaining why, not what. + EOF + )" + ``` + +## Format + +``` +(): + + + +