ProvenPin Install

RxJS 7 operators tested with TestScheduler marble diagrams: working versions (tested 2026-10-15)

Test a typeahead search stream built with debounceTime, distinctUntilChanged, switchMap and catchError, plus a retry stream, using RxJS TestScheduler marbles with virtual time.

passed4 passed · 0 failed2026-10-15 17:11 UTC
Status
passed
Last passed
2026-10-15 17:11 UTC
Tests
4 / 4
Language
TypeScript
Runtime
node v22.23.3
pinned versions3
rxjs@7.8.2@types/node@26.6.5tsx@4.23.15
run it
npm install --no-audit --no-fund --ignore-scripts=false
npm test
test environment
node v22.23.3
linux 6.17.0-1022-azure
x64
runner: github-actions 38775053802

The code

Exactly the files that passed. Your agent can pull them with get_recipe("rxjs-testscheduler-marbles") (install), or see them on GitHub.

package.json
{
  "name": "rxjs-testscheduler-marbles",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --import tsx --test test/*.test.ts"
  },
  "dependencies": {
    "rxjs": "7.8.2"
  },
  "devDependencies": {
    "@types/node": "26.6.5",
    "tsx": "4.23.15"
  }
}
src/search.ts
import { Observable, of, timer } from "rxjs";
import { catchError, debounceTime, distinctUntilChanged, filter, map, retry, switchMap } from "rxjs/operators";

export type SearchFn = (q: string) => Observable<string[]>;

/** Classic typeahead: wait for a pause, skip repeats and short terms, cancel stale requests. */
export function typeahead(input$: Observable<string>, search: SearchFn, dueTime = 30): Observable<string[]> {
  return input$.pipe(
    map((q) => q.trim()),
    debounceTime(dueTime),
    filter((q) => q.length >= 2),
    distinctUntilChanged(),
    switchMap((q) => search(q).pipe(catchError(() => of(["error"])))),
  );
}

/** Retry a failing source with a fixed delay between attempts. */
export function withRetry<T>(source$: Observable<T>, count: number, delayMs: number): Observable<T> {
  return source$.pipe(retry({ count, delay: () => timer(delayMs) }));
}
test/search.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { TestScheduler } from "rxjs/testing";
import { typeahead, withRetry } from "../src/search.ts";

const scheduler = () => new TestScheduler((actual, expected) => assert.deepEqual(actual, expected));

test("debounces, filters short terms and dedupes", () => {
  scheduler().run(({ hot, cold, expectObservable }) => {
    const input = hot("a 10ms b 40ms c 40ms d", { a: "r", b: "rx", c: "rx ", d: "rxjs" });
    const search = (q: string) => cold("-r|", { r: [q.toUpperCase()] });
    // Frames: a=0, b=11, c=52, d=93. "rx" settles at 41 and the search answers at 42;
    // "rx " trims to a duplicate; "rxjs" settles at 123 and answers at 124.
    expectObservable(typeahead(input, search)).toBe("42ms x 81ms y", { x: ["RX"], y: ["RXJS"] });
  });
});

test("switchMap cancels a slow in-flight search", () => {
  scheduler().run(({ hot, cold, expectObservable, expectSubscriptions }) => {
    const input = hot("a 40ms b", { a: "ab", b: "abc" });
    const slow = cold("50ms r|", { r: ["slow"] });
    const fast = cold("1ms r|", { r: ["fast"] });
    const search = (q: string) => (q === "ab" ? slow : fast);
    expectObservable(typeahead(input, search)).toBe("72ms x", { x: ["fast"] });
    // slow is subscribed when "ab" settles (30) and dropped when "abc" settles (71).
    expectSubscriptions(slow.subscriptions).toBe("30ms ^ 40ms !");
  });
});

test("errors become a fallback value without killing the stream", () => {
  scheduler().run(({ hot, cold, expectObservable }) => {
    const input = hot("a 40ms b", { a: "bad", b: "good" });
    const search = (q: string) => (q === "bad" ? cold("#") : cold("r|", { r: [q] }));
    expectObservable(typeahead(input, search)).toBe("30ms e 40ms g", { e: ["error"], g: ["good"] });
  });
});

test("retry resubscribes after a delay", () => {
  scheduler().run(({ cold, expectObservable, expectSubscriptions }) => {
    const source = cold("-#");
    expectObservable(withRetry(source, 2, 5)).toBe("13ms #");
    expectSubscriptions(source.subscriptions).toBe(["^!", "6ms ^!", "12ms ^!"]);
  });
});

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.