From a4eb78e287abc8b699905affc55a4fac8d7a1970 Mon Sep 17 00:00:00 2001 From: Alexander Harding Date: Fri, 16 Jan 2026 00:06:16 -0700 Subject: [PATCH 1/4] Refactor and expand observable library structure - Introduced new modules for various observable patterns including `all`, `async-subject`, `behavior-subject`, `broadcast-subject`, `defer`, `drop`, `empty`, `exhaust-map`, `filter`, `finalize`, `flat`, `flat-map`, `ignore-elements`, `map`, `materialize`, `merge`, `merge-map`, `never`, `of`, `pipe`, `race`, `replay-subject`, `switch-map`, `take`, `take-until`, `tap`, and `throw-error`. - Added comprehensive test files for each new module to ensure functionality. - Updated `deno.json` files for new modules with appropriate metadata. - Removed deprecated files and references from the common module to streamline the codebase. --- README.md | 239 +++++++++++++++++- all/README.md | 52 ++++ {web => all}/deno.json | 2 +- common/all.test.ts => all/mod.test.ts | 21 +- all/mod.ts | 101 ++++++++ async-subject/README.md | 51 ++++ async-subject/deno.json | 6 + .../mod.test.ts | 9 +- async-subject/mod.ts | 109 ++++++++ behavior-subject/README.md | 49 ++++ behavior-subject/deno.json | 6 + .../mod.test.ts | 9 +- .../mod.ts | 68 ++++- broadcast-subject/README.md | 44 ++++ broadcast-subject/deno.json | 6 + .../mod.test.ts | 10 +- .../mod.ts | 54 +++- common/README.md | 101 -------- common/all.ts | 67 ----- common/as-observable.test.ts | 45 ---- common/as-observable.ts | 20 -- common/async-subject-constructor.ts | 33 --- common/async-subject.ts | 59 ----- common/behavior-subject-constructor.ts | 52 ---- common/defer.test.ts | 57 ----- common/defer.ts | 21 -- common/dematerialize.test.ts | 68 ----- common/dematerialize.ts | 43 ---- common/deno.json | 6 - common/empty.ts | 24 -- common/merge.ts | 24 -- common/mod.test.ts | 4 - common/mod.ts | 33 --- common/never.ts | 21 -- common/observer-notification.ts | 13 - common/race.ts | 50 ---- common/replay-subject-constructor.ts | 57 ----- common/switch-map.ts | 35 --- common/throw-error.ts | 24 -- core/README.md | 107 ++++---- core/deno.json | 4 +- core/is-observable.test.ts | 2 +- core/is-observable.ts | 4 +- core/is-observer.test.ts | 2 +- core/is-observer.ts | 4 +- core/is-subject.test.ts | 2 +- core/is-subject.ts | 4 +- core/observable-constructor.ts | 10 +- core/observable.test.ts | 2 +- core/observable.ts | 10 +- core/observer-constructor.ts | 4 +- core/observer.test.ts | 2 +- core/observer.ts | 12 +- core/subject-constructor.ts | 14 +- core/subject.ts | 4 +- core/to-observable.test.ts | 2 +- core/to-observable.ts | 6 +- core/to-observer.test.ts | 2 +- core/to-observer.ts | 6 +- defer/README.md | 49 ++++ defer/deno.json | 6 + defer/mod.test.ts | 59 +++++ defer/mod.ts | 54 ++++ deno.json | 33 ++- drop/README.md | 30 +++ drop/deno.json | 6 + common/drop.test.ts => drop/mod.test.ts | 13 +- common/drop.ts => drop/mod.ts | 35 +-- empty/README.md | 27 ++ empty/deno.json | 6 + common/empty.test.ts => empty/mod.test.ts | 9 +- empty/mod.ts | 24 ++ exhaust-map/README.md | 37 +++ exhaust-map/deno.json | 6 + .../mod.test.ts | 25 +- common/exhaust-map.ts => exhaust-map/mod.ts | 23 +- filter/README.md | 29 +++ filter/deno.json | 6 + common/filter.test.ts => filter/mod.test.ts | 11 +- common/filter.ts => filter/mod.ts | 26 +- finalize/README.md | 36 +++ finalize/deno.json | 6 + .../finalize.test.ts => finalize/mod.test.ts | 11 +- common/finalize.ts => finalize/mod.ts | 37 ++- flat-map/README.md | 46 ++++ flat-map/deno.json | 6 + .../flat-map.test.ts => flat-map/mod.test.ts | 11 +- common/flat-map.ts => flat-map/mod.ts | 44 ++-- flat/README.md | 38 +++ flat/deno.json | 6 + common/flat.test.ts => flat/mod.test.ts | 11 +- common/flat.ts => flat/mod.ts | 40 +-- ignore-elements/README.md | 27 ++ ignore-elements/deno.json | 6 + .../mod.test.ts | 11 +- .../mod.ts | 22 +- internal/README.md | 6 +- internal/deno.json | 2 +- internal/is-abort-signal.ts | 2 +- internal/is-event-target.ts | 4 +- internal/is-iterable.ts | 2 +- internal/is-nil.ts | 4 +- internal/is-object.ts | 2 +- map/README.md | 30 +++ map/deno.json | 6 + common/map.test.ts => map/mod.test.ts | 13 +- common/map.ts => map/mod.ts | 26 +- materialize/README.md | 72 ++++++ materialize/deno.json | 6 + .../mod.test.ts | 11 +- common/materialize.ts => materialize/mod.ts | 29 ++- merge-map/README.md | 45 ++++ merge-map/deno.json | 6 + .../mod.test.ts | 13 +- common/merge-map.ts => merge-map/mod.ts | 46 +++- merge/README.md | 39 +++ merge/deno.json | 6 + common/merge.test.ts => merge/mod.test.ts | 11 +- merge/mod.ts | 53 ++++ never/README.md | 23 ++ never/deno.json | 6 + common/never.test.ts => never/mod.test.ts | 9 +- never/mod.ts | 21 ++ of/README.md | 54 ++++ of/deno.json | 6 + common/of.test.ts => of/mod.test.ts | 9 +- common/of.ts => of/mod.ts | 28 +- pipe/README.md | 34 +++ pipe/deno.json | 6 + common/pipe.test.ts => pipe/mod.test.ts | 2 +- common/pipe.ts => pipe/mod.ts | 2 +- race/README.md | 38 +++ race/deno.json | 6 + common/race.test.ts => race/mod.test.ts | 21 +- race/mod.ts | 76 ++++++ replay-subject/README.md | 54 ++++ replay-subject/deno.json | 6 + .../mod.test.ts | 13 +- .../mod.ts | 69 ++++- switch-map/README.md | 38 +++ switch-map/deno.json | 6 + .../mod.test.ts | 25 +- switch-map/mod.ts | 54 ++++ take-until/README.md | 35 +++ take-until/deno.json | 6 + .../mod.test.ts | 13 +- common/take-until.ts => take-until/mod.ts | 35 ++- take/README.md | 29 +++ take/deno.json | 6 + common/take.test.ts => take/mod.test.ts | 17 +- common/take.ts => take/mod.ts | 28 +- tap/README.md | 44 ++++ tap/deno.json | 6 + common/tap.test.ts => tap/mod.test.ts | 15 +- common/tap.ts => tap/mod.ts | 27 +- throw-error/README.md | 24 ++ throw-error/deno.json | 6 + .../mod.test.ts | 9 +- throw-error/mod.ts | 24 ++ timer/README.md | 64 +++++ timer/deno.json | 6 + common/timer.test.ts => timer/mod.test.ts | 13 +- common/timer.ts => timer/mod.ts | 26 +- web/README.md | 4 - web/broadcast-subject-constructor.ts | 48 ---- web/mod.test.ts | 4 - web/mod.ts | 2 - 167 files changed, 2855 insertions(+), 1444 deletions(-) create mode 100644 all/README.md rename {web => all}/deno.json (67%) rename common/all.test.ts => all/mod.test.ts (84%) create mode 100644 all/mod.ts create mode 100644 async-subject/README.md create mode 100644 async-subject/deno.json rename common/async-subject.test.ts => async-subject/mod.test.ts (96%) create mode 100644 async-subject/mod.ts create mode 100644 behavior-subject/README.md create mode 100644 behavior-subject/deno.json rename common/behavior-subject.test.ts => behavior-subject/mod.test.ts (97%) rename common/behavior-subject.ts => behavior-subject/mod.ts (61%) create mode 100644 broadcast-subject/README.md create mode 100644 broadcast-subject/deno.json rename web/broadcast-subject.test.ts => broadcast-subject/mod.test.ts (97%) rename web/broadcast-subject.ts => broadcast-subject/mod.ts (51%) delete mode 100644 common/README.md delete mode 100644 common/all.ts delete mode 100644 common/as-observable.test.ts delete mode 100644 common/as-observable.ts delete mode 100644 common/async-subject-constructor.ts delete mode 100644 common/async-subject.ts delete mode 100644 common/behavior-subject-constructor.ts delete mode 100644 common/defer.test.ts delete mode 100644 common/defer.ts delete mode 100644 common/dematerialize.test.ts delete mode 100644 common/dematerialize.ts delete mode 100644 common/deno.json delete mode 100644 common/empty.ts delete mode 100644 common/merge.ts delete mode 100644 common/mod.test.ts delete mode 100644 common/mod.ts delete mode 100644 common/never.ts delete mode 100644 common/observer-notification.ts delete mode 100644 common/race.ts delete mode 100644 common/replay-subject-constructor.ts delete mode 100644 common/switch-map.ts delete mode 100644 common/throw-error.ts create mode 100644 defer/README.md create mode 100644 defer/deno.json create mode 100644 defer/mod.test.ts create mode 100644 defer/mod.ts create mode 100644 drop/README.md create mode 100644 drop/deno.json rename common/drop.test.ts => drop/mod.test.ts (83%) rename common/drop.ts => drop/mod.ts (55%) create mode 100644 empty/README.md create mode 100644 empty/deno.json rename common/empty.test.ts => empty/mod.test.ts (84%) create mode 100644 empty/mod.ts create mode 100644 exhaust-map/README.md create mode 100644 exhaust-map/deno.json rename common/exhaust-map.test.ts => exhaust-map/mod.test.ts (96%) rename common/exhaust-map.ts => exhaust-map/mod.ts (61%) create mode 100644 filter/README.md create mode 100644 filter/deno.json rename common/filter.test.ts => filter/mod.test.ts (87%) rename common/filter.ts => filter/mod.ts (65%) create mode 100644 finalize/README.md create mode 100644 finalize/deno.json rename common/finalize.test.ts => finalize/mod.test.ts (88%) rename common/finalize.ts => finalize/mod.ts (51%) create mode 100644 flat-map/README.md create mode 100644 flat-map/deno.json rename common/flat-map.test.ts => flat-map/mod.test.ts (93%) rename common/flat-map.ts => flat-map/mod.ts (71%) create mode 100644 flat/README.md create mode 100644 flat/deno.json rename common/flat.test.ts => flat/mod.test.ts (90%) rename common/flat.ts => flat/mod.ts (52%) create mode 100644 ignore-elements/README.md create mode 100644 ignore-elements/deno.json rename common/ignore-elements.test.ts => ignore-elements/mod.test.ts (84%) rename common/ignore-elements.ts => ignore-elements/mod.ts (60%) create mode 100644 map/README.md create mode 100644 map/deno.json rename common/map.test.ts => map/mod.test.ts (88%) rename common/map.ts => map/mod.ts (70%) create mode 100644 materialize/README.md create mode 100644 materialize/deno.json rename common/materialize.test.ts => materialize/mod.test.ts (89%) rename common/materialize.ts => materialize/mod.ts (74%) create mode 100644 merge-map/README.md create mode 100644 merge-map/deno.json rename common/merge-map.test.ts => merge-map/mod.test.ts (95%) rename common/merge-map.ts => merge-map/mod.ts (53%) create mode 100644 merge/README.md create mode 100644 merge/deno.json rename common/merge.test.ts => merge/mod.test.ts (68%) create mode 100644 merge/mod.ts create mode 100644 never/README.md create mode 100644 never/deno.json rename common/never.test.ts => never/mod.test.ts (80%) create mode 100644 never/mod.ts create mode 100644 of/README.md create mode 100644 of/deno.json rename common/of.test.ts => of/mod.test.ts (78%) rename common/of.ts => of/mod.ts (66%) create mode 100644 pipe/README.md create mode 100644 pipe/deno.json rename common/pipe.test.ts => pipe/mod.test.ts (87%) rename common/pipe.ts => pipe/mod.ts (99%) create mode 100644 race/README.md create mode 100644 race/deno.json rename common/race.test.ts => race/mod.test.ts (84%) create mode 100644 race/mod.ts create mode 100644 replay-subject/README.md create mode 100644 replay-subject/deno.json rename common/replay-subject.test.ts => replay-subject/mod.test.ts (95%) rename common/replay-subject.ts => replay-subject/mod.ts (54%) create mode 100644 switch-map/README.md create mode 100644 switch-map/deno.json rename common/switch-map.test.ts => switch-map/mod.test.ts (96%) create mode 100644 switch-map/mod.ts create mode 100644 take-until/README.md create mode 100644 take-until/deno.json rename common/take-until.test.ts => take-until/mod.test.ts (91%) rename common/take-until.ts => take-until/mod.ts (58%) create mode 100644 take/README.md create mode 100644 take/deno.json rename common/take.test.ts => take/mod.test.ts (84%) rename common/take.ts => take/mod.ts (67%) create mode 100644 tap/README.md create mode 100644 tap/deno.json rename common/tap.test.ts => tap/mod.test.ts (90%) rename common/tap.ts => tap/mod.ts (81%) create mode 100644 throw-error/README.md create mode 100644 throw-error/deno.json rename common/throw-error.test.ts => throw-error/mod.test.ts (68%) create mode 100644 throw-error/mod.ts create mode 100644 timer/README.md create mode 100644 timer/deno.json rename common/timer.test.ts => timer/mod.test.ts (95%) rename common/timer.ts => timer/mod.ts (77%) delete mode 100644 web/README.md delete mode 100644 web/broadcast-subject-constructor.ts delete mode 100644 web/mod.test.ts delete mode 100644 web/mod.ts diff --git a/README.md b/README.md index 6c4c258..4c36673 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,236 @@ -A collection of lightweight, RxJS-inspired implementation of the Observer pattern in JavaScript. -Features Observable's with AbortController-based unsubscription, supporting both synchronous and -asynchronous producers. +# [@observable](https://jsr.io/@observable) Monorepo + +A set of lightweight, modern reactive libraries inspired by [RxJS](https://rxjs.dev/), implementing +the [Observer pattern](https://refactoring.guru/design-patterns/observer) in JavaScript. + +## What You'll Find Here + +- A tribute to [RxJS](https://rxjs.dev/), built for real-world use. +- Carefully crafted utilities and primitives by a solo developer who loves reactive programming. + +## What You Won't Find Here + +- A drop-in replacement for [RxJS](https://rxjs.dev/). [RxJS](https://rxjs.dev/) remains the gold + standard for enterprise-scale projects, while this monorepo aims for simplicity and focus. + +## Repository Expectations + +### SOLID Principles + +Adhere to the SOLID design principles as much as possible. We could say a lot, but will defer to +myriad of online resources that outline the merits of these principles. + +- **Single Responsibility** +- **Open/Closed** +- **Liskov Substitution** +- **Interface Segregation** +- **Dependency Inversion** + +### Composition Over Inheritance + +Class definitions should be expressions that leverage +[declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) and +private API obfuscation to achieve pure encapsulation. + +```ts +interface Example { + foo(): void; + bar(): void; +} + +interface ExampleConstructor { + new (): A; + readonly prototype: A; +} + +const Example: ExampleConstructor = class { + foo(): void { + this.#foo(); + } + + #foo(): void { + // Do something + } + + bar(): void { + this.#bar(); + } + + #bar(): void { + // Do something + } +}; +``` + +### Immutability + +All object instances, constructors, and prototypes **must be frozen** to prevent runtime mutation. +This ensures predictable behavior and guards against accidental modification. + +```ts +const Example: ExampleConstructor = class { + constructor() { + Object.freeze(this); + } +}; + +Object.freeze(Example); +Object.freeze(Example.prototype); +``` + +### Runtime Argument Validation + +All public functions and methods **must validate their arguments** at runtime using the standard +`TypeError` type. + +```ts +function example(value: string): void { + if (arguments.length === 0) { + throw new TypeError("1 argument required but 0 present"); + } + if (typeof value !== "string") { + throw new TypeError("Parameter 1 is not of type 'String'"); + } +} +``` + +All public methods **must validate their 'this' instance** at runtime using the standard `TypeError` +type. + +```ts +const Example: ExampleConstructor = class { + foo() { + if (!(this instanceof Example)) { + throw new TypeError("'this' is not instanceof 'Example'"); + } + } +}; +``` + +### Shallow Call Stacks for Debugging + +Validation must occur **at the entry point** of each public function, not delegated to shared +validation helpers buried in the call stack. This keeps stack traces shallow and points errors +directly to the user's call site, making debugging straightforward. + +```ts +// ✓ Good: Validation at entry point produces a shallow stack trace +function map(transform: (value: In) => Out): (source: In[]) => Out[] { + if (arguments.length === 0) throw new MinimumArgumentsRequiredError(); + if (typeof transform !== "function") { + throw new ParameterTypeError(0, "Function"); + } + return function mapFn(source) { + if (arguments.length === 0) throw new MinimumArgumentsRequiredError(); + if (!Array.isArray(source)) throw new ParameterTypeError(0, "Array"); + // ... + }; +} + +// ✗ Bad: Delegating to a validation helper adds noise to the stack trace +function map(transform: (value: In) => Out) { + validateFunction(transform); // Adds extra frame(s) to stack + return function mapFn(source) { + validateArray(source); // Adds extra frame(s) to stack + // ... + }; +} +``` + +When a user passes invalid arguments, the error should point directly to their code — not to +internal library plumbing. + +### Type Guards + +Provide `is*` type guard functions for all unique public interfaces. Type guards must: + +- Accept `unknown` and return a type predicate +- Support both class instances and POJOs +- Validate required arguments + +```ts +function isUser(value: unknown): value is User { + if (arguments.length === 0) throw new MinimumArgumentsRequiredError(); + return ( + value instanceof User || + (isObject(value) && + "id" in value && + typeof value.id === "string" && + "name" in value && + typeof value.name === "string") + ); +} +``` + +### Symbol.toStringTag + +All classes **must implement `Symbol.toStringTag`** for proper object stringification. + +```ts +const Example: ExampleConstructor = class { + readonly [Symbol.toStringTag] = "Example"; +}; + +// Usage: +`${new Example()}`; // "[object Example]" +``` + +### Pure Functions + +Functions should be pure wherever possible: + +- **Deterministic** — Same inputs always produce the same output +- **No side effects** — No mutations, I/O, or external state changes +- **Referentially transparent** — Can be replaced with its return value + +### Documentation + +All public APIs **must have JSDoc documentation** including: + +- A description of purpose and behavior +- `@example` blocks with runnable code samples + +````ts +/** + * Calculates the sum of all numbers in an array. + * @example + * ```ts + * sum([1, 2, 3]); // 6 + * sum([]); // 0 + * ``` + */ +export function sum(values: number[]): number; +```` + +### Testing + +Tests follow the **Arrange/Act/Assert** pattern with descriptive test names that explain the +expected behavior. Each test should focus on a single behavior. + +```ts +Deno.test("sum should return 0 for an empty array", () => { + // Arrange + const values: Array = []; + + // Act + const result = sum(values); + + // Assert + assertEquals(result, 0); +}); +``` + +### Internal Utilities + +Reusable utilities should be centralized in a dedicated internal package/module. Internal functions +that should not be exported are marked with `@internal Do NOT export.` in their JSDoc. + +```ts +/** + * Clamps a value between a minimum and maximum. + * @internal Do NOT export. + */ +function clamp(value: number, min: number, max: number): number { + return Math.min(Math.max(value, min), max); +} +``` diff --git a/all/README.md b/all/README.md new file mode 100644 index 0000000..ffaa57e --- /dev/null +++ b/all/README.md @@ -0,0 +1,52 @@ +# @observable/all + +Creates and returns an [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) whose +[`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values are calculated from the +latest [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values of each of its +[sources](https://jsr.io/@observable/core#source). If any of the +[sources](https://jsr.io/@observable/core#source) are empty, the returned +[`Observable`](https://jsr.io/@observable/core/doc/~/Observable) will also be empty. + +## Example + +```ts +import { all } from "@observable/all"; +import { of } from "@observable/of"; + +const controller = new AbortController(); +all([of([1, 2, 3]), of([4, 5, 6]), of([7, 8, 9])]).subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), + return: () => console.log("return"), + throw: (value) => console.log("throw", value), +}); + +// Console output: +// "next" [3, 6, 7] +// "next" [3, 6, 8] +// "next" [3, 6, 9] +// "return" +``` + +## Example with empty source + +```ts +import { all } from "@observable/all"; +import { of } from "@observable/of"; +import { empty } from "@observable/empty"; + +const controller = new AbortController(); +all([of([1, 2, 3]), empty, of([7, 8, 9])]).subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), + return: () => console.log("return"), + throw: (value) => console.log("throw", value), +}); + +// Console output: +// "return" +``` + +# Glossary And Semantics + +[@observable/core](https://jsr.io/@observable/core#glossary-and-semantics) diff --git a/web/deno.json b/all/deno.json similarity index 67% rename from web/deno.json rename to all/deno.json index ad25663..2dbe8ef 100644 --- a/web/deno.json +++ b/all/deno.json @@ -1,5 +1,5 @@ { - "name": "@xan/observable-web", + "name": "@observable/all", "version": "0.1.0", "license": "MIT", "exports": "./mod.ts" diff --git a/common/all.test.ts b/all/mod.test.ts similarity index 84% rename from common/all.test.ts rename to all/mod.test.ts index 4ca4316..7bc8363 100644 --- a/common/all.test.ts +++ b/all/mod.test.ts @@ -1,15 +1,14 @@ import { assertEquals, assertStrictEquals } from "@std/assert"; -import { Observer } from "@xan/observable-core"; -import { materialize } from "./materialize.ts"; -import type { ObserverNotification } from "./observer-notification.ts"; -import { flat } from "./flat.ts"; -import { defer } from "./defer.ts"; -import { empty } from "./empty.ts"; -import { never } from "./never.ts"; -import { of } from "./of.ts"; -import { pipe } from "./pipe.ts"; -import { all } from "./all.ts"; -import { ReplaySubject } from "./replay-subject.ts"; +import { Observer } from "@observable/core"; +import { materialize, type ObserverNotification } from "@observable/materialize"; +import { flat } from "@observable/flat"; +import { defer } from "@observable/defer"; +import { empty } from "@observable/empty"; +import { never } from "@observable/never"; +import { of } from "@observable/of"; +import { pipe } from "@observable/pipe"; +import { all } from "./mod.ts"; +import { ReplaySubject } from "@observable/replay-subject"; Deno.test( "all should multiple sources that next and return synchronously", diff --git a/all/mod.ts b/all/mod.ts new file mode 100644 index 0000000..658a6fd --- /dev/null +++ b/all/mod.ts @@ -0,0 +1,101 @@ +import { type Observable, Observer, Subject } from "@observable/core"; +import { MinimumArgumentsRequiredError, noop, ParameterTypeError } from "@observable/internal"; +import { defer } from "@observable/defer"; +import { empty } from "@observable/empty"; +import { of } from "@observable/of"; +import { pipe } from "@observable/pipe"; +import { tap } from "@observable/tap"; +import { map } from "@observable/map"; +import { mergeMap } from "@observable/merge-map"; +import { filter } from "@observable/filter"; +import { takeUntil } from "@observable/take-until"; + +/** + * Creates and returns an [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) whose + * [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values are calculated from the latest + * [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values of each of its [sources](https://jsr.io/@observable/core#source). + * If any of the [sources](https://jsr.io/@observable/core#source) are empty, the returned + * [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) will also be empty. + * @example + * ```ts + * import { all } from "@observable/all"; + * import { of } from "@observable/of"; + * + * const controller = new AbortController(); + * all([of([1, 2, 3]), of([4, 5, 6]), of([7, 8, 9])]).subscribe({ + * signal: controller.signal, + * next: (value) => console.log("next", value), + * return: () => console.log("return"), + * throw: (value) => console.log("throw", value), + * }); + * + * // Console output: + * // "next" [3, 6, 7] + * // "next" [3, 6, 8] + * // "next" [3, 6, 9] + * // "return" + * @example + * ```ts + * import { all } from "@observable/all"; + * import { of } from "@observable/of"; + * import { empty } from "@observable/empty"; + * + * const controller = new AbortController(); + * all([of([1, 2, 3]), empty, of([7, 8, 9])]).subscribe({ + * signal: controller.signal, + * next: (value) => console.log("next", value), + * return: () => console.log("return"), + * throw: (value) => console.log("throw", value), + * }); + * + * // Console output: + * // "return" + */ +export function all>( + sources: Readonly<{ [Key in keyof Values]: Observable }>, +): Observable; +export function all( + // Long term, it would be nice to be able to accept an Iterable for performance and flexibility. + // This new signature would have to work in conjunction with the mapped array signature above as this + // encourages more explicit types for sources as a tuple. + sources: ReadonlyArray, +): Observable> { + if (arguments.length === 0) throw new MinimumArgumentsRequiredError(); + if (!Array.isArray(sources)) throw new ParameterTypeError(0, "Array"); + if (sources.length === 0) return empty; + return defer(() => { + let receivedFirstValueCount = 0; + const { length: expectedFirstValueCount } = sources; + const values: Array = []; + const emptySourceNotifier = new Subject(); + return pipe( + of(sources), + mergeMap((source, index) => { + let isEmpty = true; + return pipe( + source, + tap( + new Observer({ + next: processNextValue, + return: processReturn, + throw: noop, + }), + ), + ); + + function processNextValue(value: unknown): void { + if (isEmpty) receivedFirstValueCount++; + isEmpty = false; + values[index] = value; + } + + function processReturn(): void { + if (isEmpty) emptySourceNotifier.next(); + } + }), + filter(() => receivedFirstValueCount === expectedFirstValueCount), + map(() => values.slice()), + takeUntil(emptySourceNotifier), + ); + }); +} diff --git a/async-subject/README.md b/async-subject/README.md new file mode 100644 index 0000000..9815f25 --- /dev/null +++ b/async-subject/README.md @@ -0,0 +1,51 @@ +# @observable/async-subject + +A variant of [`Subject`](https://jsr.io/@observable/core/doc/~/Subject) that buffers the most recent +[`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed value until +[`return`](https://jsr.io/@observable/core/doc/~/Observer.return) is called. Once +[`return`](https://jsr.io/@observable/core/doc/~/Observer.return)ed, +[`next`](https://jsr.io/@observable/core/doc/~/Observer.next) will be replayed to late +[`consumers`](https://jsr.io/@observable/core#consumer) upon +[`subscription`](https://jsr.io/@observable/core/doc/~/Observable.subscribe). + +## Example + +```ts +import { AsyncSubject } from "@observable/async-subject"; + +const subject = new AsyncSubject(); +const controller = new AbortController(); + +subject.next(1); +subject.next(2); + +subject.subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), + return: () => console.log("return"), + throw: (value) => console.log("throw", value), +}); + +subject.next(3); + +subject.return(); + +// Console output: +// "next" 3 +// "return" + +subject.subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), + return: () => console.log("return"), + throw: (value) => console.log("throw", value), +}); + +// Console output: +// "next" 3 +// "return" +``` + +# Glossary And Semantics + +[@observable/core](https://jsr.io/@observable/core#glossary-and-semantics) diff --git a/async-subject/deno.json b/async-subject/deno.json new file mode 100644 index 0000000..0fe69f4 --- /dev/null +++ b/async-subject/deno.json @@ -0,0 +1,6 @@ +{ + "name": "@observable/async-subject", + "version": "0.1.0", + "license": "MIT", + "exports": "./mod.ts" +} diff --git a/common/async-subject.test.ts b/async-subject/mod.test.ts similarity index 96% rename from common/async-subject.test.ts rename to async-subject/mod.test.ts index cad51f5..52bbb31 100644 --- a/common/async-subject.test.ts +++ b/async-subject/mod.test.ts @@ -1,9 +1,8 @@ import { assertEquals, assertStrictEquals, assertThrows } from "@std/assert"; -import { Observable, Observer } from "@xan/observable-core"; -import { AsyncSubject } from "./async-subject.ts"; -import { materialize } from "./materialize.ts"; -import type { ObserverNotification } from "./observer-notification.ts"; -import { pipe } from "./pipe.ts"; +import { Observable, Observer } from "@observable/core"; +import { AsyncSubject } from "./mod.ts"; +import { materialize, type ObserverNotification } from "@observable/materialize"; +import { pipe } from "@observable/pipe"; Deno.test("AsyncSubject.toString should be '[object AsyncSubject]'", () => { // Arrange / Act / Assert diff --git a/async-subject/mod.ts b/async-subject/mod.ts new file mode 100644 index 0000000..484be23 --- /dev/null +++ b/async-subject/mod.ts @@ -0,0 +1,109 @@ +import { isObserver, type Observer, type Subject } from "@observable/core"; +import { + InstanceofError, + MinimumArgumentsRequiredError, + ParameterTypeError, +} from "@observable/internal"; +import { flat } from "@observable/flat"; +import { pipe } from "@observable/pipe"; +import { ignoreElements } from "@observable/ignore-elements"; +import { ReplaySubject } from "@observable/replay-subject"; + +/** + * Object type that acts as a variant of [`Subject`](https://jsr.io/@observable/core/doc/~/Subject). + */ +export type AsyncSubject = Subject; + +/** + * Object interface for an {@linkcode AsyncSubject} factory. + */ +export interface AsyncSubjectConstructor { + /** + * Creates and returns an object that acts as a [`Subject`](https://jsr.io/@observable/core/doc/~/Subject) that buffers the most recent + * [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed value until [`return`](https://jsr.io/@observable/core/doc/~/Observer.return) is called. + * Once [`return`](https://jsr.io/@observable/core/doc/~/Observer.return)ed, [`next`](https://jsr.io/@observable/core/doc/~/Observer.next) will be replayed + * to late [`consumers`](https://jsr.io/@observable/core#consumer) upon [`subscription`](https://jsr.io/@observable/core/doc/~/Observable.subscribe). + * @example + * ```ts + * import { AsyncSubject } from "@observable/async-subject"; + * + * const subject = new AsyncSubject(); + * const controller = new AbortController(); + * + * subject.next(1); + * subject.next(2); + * + * subject.subscribe({ + * signal: controller.signal, + * next: (value) => console.log("next", value), + * return: () => console.log("return"), + * throw: (value) => console.log("throw", value), + * }); + * + * subject.next(3); + * + * subject.return(); + * + * // Console output: + * // "next" 3 + * // "return" + * + * subject.subscribe({ + * signal: controller.signal, + * next: (value) => console.log("next", value), + * return: () => console.log("return"), + * throw: (value) => console.log("throw", value), + * }); + * + * // Console output: + * // "next" 3 + * // "return" + * ``` + */ + new (): AsyncSubject; + new (): AsyncSubject; + readonly prototype: AsyncSubject; +} + +export const AsyncSubject: AsyncSubjectConstructor = class { + readonly [Symbol.toStringTag] = "AsyncSubject"; + readonly #subject = new ReplaySubject(1); + readonly signal = this.#subject.signal; + readonly #observable = flat([ + pipe(this.#subject, ignoreElements()), + this.#subject, + ]); + + constructor() { + Object.freeze(this); + } + + next(value: unknown): void { + if (this instanceof AsyncSubject) this.#subject.next(value); + else throw new InstanceofError("this", "AsyncSubject"); + } + + return(): void { + if (this instanceof AsyncSubject) this.#subject.return(); + else throw new InstanceofError("this", "AsyncSubject"); + } + + throw(value: unknown): void { + if (this instanceof AsyncSubject) { + this.#subject.next(undefined); + this.#subject.throw(value); + } else throw new InstanceofError("this", "AsyncSubject"); + } + + subscribe(observer: Observer): void { + if (!(this instanceof AsyncSubject)) { + throw new InstanceofError("this", "AsyncSubject"); + } + if (arguments.length === 0) throw new MinimumArgumentsRequiredError(); + if (!isObserver(observer)) throw new ParameterTypeError(0, "Observer"); + this.#observable.subscribe(observer); + } +}; + +Object.freeze(AsyncSubject); +Object.freeze(AsyncSubject.prototype); diff --git a/behavior-subject/README.md b/behavior-subject/README.md new file mode 100644 index 0000000..41ce7a8 --- /dev/null +++ b/behavior-subject/README.md @@ -0,0 +1,49 @@ +# @observable/behavior-subject + +A variant of [`Subject`](https://jsr.io/@observable/core/doc/~/Subject) that keeps track of its +current value and replays it to [`consumers`](https://jsr.io/@observable/core#consumer) upon +[`subscription`](https://jsr.io/@observable/core/doc/~/Observable.subscribe). + +## Example + +```ts +import { BehaviorSubject } from "@observable/behavior-subject"; + +const subject = new BehaviorSubject(0); +const controller = new AbortController(); + +subject.subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), + return: () => console.log("return"), + throw: () => console.error("throw"), +}); + +// console output: +// "next" 0 + +subject.next(1); + +// console output: +// "next" 1 + +subject.return(); + +// console output: +// "return" + +subject.subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), + return: () => console.log("return"), + throw: () => console.error("throw"), +}); + +// console output: +// "next" 1 +// "return" +``` + +# Glossary And Semantics + +[@observable/core](https://jsr.io/@observable/core#glossary-and-semantics) diff --git a/behavior-subject/deno.json b/behavior-subject/deno.json new file mode 100644 index 0000000..4620440 --- /dev/null +++ b/behavior-subject/deno.json @@ -0,0 +1,6 @@ +{ + "name": "@observable/behavior-subject", + "version": "0.1.0", + "license": "MIT", + "exports": "./mod.ts" +} diff --git a/common/behavior-subject.test.ts b/behavior-subject/mod.test.ts similarity index 97% rename from common/behavior-subject.test.ts rename to behavior-subject/mod.test.ts index 51504f6..8a41b37 100644 --- a/common/behavior-subject.test.ts +++ b/behavior-subject/mod.test.ts @@ -1,9 +1,8 @@ import { assertEquals, assertStrictEquals, assertThrows } from "@std/assert"; -import { Observable, Observer } from "@xan/observable-core"; -import { materialize } from "./materialize.ts"; -import type { ObserverNotification } from "./observer-notification.ts"; -import { pipe } from "./pipe.ts"; -import { BehaviorSubject, isBehaviorSubject } from "./behavior-subject.ts"; +import { Observable, Observer } from "@observable/core"; +import { materialize, type ObserverNotification } from "@observable/materialize"; +import { pipe } from "@observable/pipe"; +import { BehaviorSubject, isBehaviorSubject } from "./mod.ts"; Deno.test("BehaviorSubject.value should return current value", () => { // Arrange diff --git a/common/behavior-subject.ts b/behavior-subject/mod.ts similarity index 61% rename from common/behavior-subject.ts rename to behavior-subject/mod.ts index f92ead0..aa19b6a 100644 --- a/common/behavior-subject.ts +++ b/behavior-subject/mod.ts @@ -1,21 +1,71 @@ -import { isObserver, isSubject, Observer, type Subject } from "@xan/observable-core"; +import { isObserver, isSubject, Observer, type Subject } from "@observable/core"; import { InstanceofError, MinimumArgumentsRequiredError, ParameterTypeError, -} from "@xan/observable-internal"; -import { pipe } from "./pipe.ts"; -import { take } from "./take.ts"; -import { ReplaySubject } from "./replay-subject.ts"; -import type { BehaviorSubjectConstructor } from "./behavior-subject-constructor.ts"; +} from "@observable/internal"; +import { pipe } from "@observable/pipe"; +import { take } from "@observable/take"; +import { ReplaySubject } from "@observable/replay-subject"; /** - * Object type that acts as a variant of [`Subject`](https://jsr.io/@xan/observable-core/doc/~/Subject). + * Object type that acts as a variant of [`Subject`](https://jsr.io/@observable/core/doc/~/Subject). */ export type BehaviorSubject = & Subject & Readonly>; +/** + * Object interface for a {@linkcode BehaviorSubject} factory. + */ +export interface BehaviorSubjectConstructor { + /** + * Creates and returns an object that acts as a [`Subject`](https://jsr.io/@observable/core/doc/~/Subject) that keeps track of it's current + * value and replays it to [`consumers`](https://jsr.io/@observable/core#consumer) upon + * [`subscription`](https://jsr.io/@observable/core/doc/~/Observable.subscribe). + * @example + * ```ts + * import { BehaviorSubject } from "@observable/behavior-subject"; + * + * const subject = new BehaviorSubject(0); + * const controller = new AbortController(); + * + * subject.subscribe({ + * signal: controller.signal, + * next: (value) => console.log("next", value), + * return: () => console.log("return"), + * throw: () => console.error("throw"), + * }); + * + * // console output: + * // "next" 0 + * + * subject.next(1); + * + * // console output: + * // "next" 1 + * + * subject.return(); + * + * // console output: + * // "return" + * + * subject.subscribe({ + * signal: controller.signal, + * next: (value) => console.log("next", value), + * return: () => console.log("return"), + * throw: () => console.error("throw"), + * }); + * + * // console output: + * // "next" 1 + * // "return" + * ``` + */ + new (value: Value): BehaviorSubject; + readonly prototype: BehaviorSubject; +} + export const BehaviorSubject: BehaviorSubjectConstructor = class { readonly [Symbol.toStringTag] = "BehaviorSubject"; readonly #subject = new ReplaySubject(1); @@ -67,7 +117,7 @@ Object.freeze(BehaviorSubject.prototype); * Checks if a {@linkcode value} is an object that implements the {@linkcode BehaviorSubject} interface. * @example * ```ts - * import { isBehaviorSubject, BehaviorSubject } from "@xan/observable-common"; + * import { isBehaviorSubject, BehaviorSubject } from "@observable/behavior-subject"; * * const subject = new BehaviorSubject(0); * @@ -75,7 +125,7 @@ Object.freeze(BehaviorSubject.prototype); * ``` * @example * ```ts - * import { isBehaviorSubject, BehaviorSubject } from "@xan/observable-common"; + * import { isBehaviorSubject, BehaviorSubject } from "@observable/behavior-subject"; * * const custom: BehaviorSubject = { * value: 0, diff --git a/broadcast-subject/README.md b/broadcast-subject/README.md new file mode 100644 index 0000000..aa75777 --- /dev/null +++ b/broadcast-subject/README.md @@ -0,0 +1,44 @@ +# @observable/broadcast-subject + +A variant of [`Subject`](https://jsr.io/@observable/core/doc/~/Subject). When values are +[`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed, they are +[`structuredClone`](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone)d and sent only +to [consumers](https://jsr.io/@observable/core#consumer) of _other_ `BroadcastSubject` instances +with the same name even if they are in different browsing contexts (e.g. browser tabs). Logically, +[consumers](https://jsr.io/@observable/core#consumer) of the `BroadcastSubject` do not receive its +_own_ [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values. + +## Example + +```ts +import { BroadcastSubject } from "@observable/broadcast-subject"; + +// Setup subjects +const name = "test"; +const controller = new AbortController(); +const subject1 = new BroadcastSubject(name); +const subject2 = new BroadcastSubject(name); + +// Subscribe to subjects +subject1.subscribe({ + signal: controller.signal, + next: (value) => console.log("subject1 received", value, "from subject1"), + return: () => console.log("subject1 returned"), + throw: (value) => console.log("subject1 threw", value), +}); +subject2.subscribe({ + signal: controller.signal, + next: (value) => console.log("subject2 received", value, "from subject2"), + return: () => console.log("subject2 returned"), + throw: (value) => console.log("subject2 threw", value), +}); + +subject1.next(1); // subject2 received 1 from subject1 +subject2.next(2); // subject1 received 2 from subject2 +subject2.return(); // subject2 returned +subject1.next(3); // No console output since subject2 is already returned +``` + +# Glossary And Semantics + +[@observable/core](https://jsr.io/@observable/core#glossary-and-semantics) diff --git a/broadcast-subject/deno.json b/broadcast-subject/deno.json new file mode 100644 index 0000000..b2620df --- /dev/null +++ b/broadcast-subject/deno.json @@ -0,0 +1,6 @@ +{ + "name": "@observable/broadcast-subject", + "version": "0.1.0", + "license": "MIT", + "exports": "./mod.ts" +} diff --git a/web/broadcast-subject.test.ts b/broadcast-subject/mod.test.ts similarity index 97% rename from web/broadcast-subject.test.ts rename to broadcast-subject/mod.test.ts index 09a4f0a..7f6dc61 100644 --- a/web/broadcast-subject.test.ts +++ b/broadcast-subject/mod.test.ts @@ -1,8 +1,10 @@ -import { Observer } from "@xan/observable-core"; -import { materialize, type ObserverNotification, of, pipe } from "@xan/observable-common"; -import { BroadcastSubject } from "./broadcast-subject.ts"; +import { Observer } from "@observable/core"; +import { of } from "@observable/of"; +import { BroadcastSubject } from "./mod.ts"; import { assertEquals, assertInstanceOf, assertStrictEquals, assertThrows } from "@std/assert"; -import { noop } from "@xan/observable-internal"; +import { noop } from "@observable/internal"; +import { pipe } from "@observable/pipe"; +import { materialize, type ObserverNotification } from "@observable/materialize"; Deno.test( "BroadcastSubject.toString should be '[object BroadcastSubject]'", diff --git a/web/broadcast-subject.ts b/broadcast-subject/mod.ts similarity index 51% rename from web/broadcast-subject.ts rename to broadcast-subject/mod.ts index 0c78e12..efde284 100644 --- a/web/broadcast-subject.ts +++ b/broadcast-subject/mod.ts @@ -1,16 +1,62 @@ -import { isObserver, type Observer, Subject } from "@xan/observable-core"; +import { isObserver, type Observer, Subject } from "@observable/core"; import { InstanceofError, MinimumArgumentsRequiredError, ParameterTypeError, -} from "@xan/observable-internal"; -import type { BroadcastSubjectConstructor } from "./broadcast-subject-constructor.ts"; +} from "@observable/internal"; /** - * Object type that acts as a variant of [`Subject`](https://jsr.io/@xan/observable-core/doc/~/Subject). + * Object type that acts as a variant of [`Subject`](https://jsr.io/@observable/core/doc/~/Subject). */ export type BroadcastSubject = Subject; +/** + * Object interface for an {@linkcode BroadcastSubject} factory. + */ +export interface BroadcastSubjectConstructor { + /** + * Creates and returns a variant of [`Subject`](https://jsr.io/@xan/subject/doc/~/Subject). When values + * are [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed, they are + * {@linkcode structuredClone|structured cloned} and sent only to [consumers](https://jsr.io/@observable/core#consumer) + * of _other_ {@linkcode BroadcastSubject} instances with the same {@linkcode name} even if they are in different browsing + * contexts (e.g. browser tabs). Logically, [consumers](https://jsr.io/@observable/core#consumer) of the + * {@linkcode BroadcastSubject} do not receive it's _own_ + * [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values. + * @example + * ```ts + * import { BroadcastSubject } from "@observable/broadcast-subject"; + * + * // Setup subjects + * const name = "test"; + * const controller = new AbortController(); + * const subject1 = new BroadcastSubject(name); + * const subject2 = new BroadcastSubject(name); + * + * // Subscribe to subjects + * subject1.subscribe({ + * signal: controller.signal, + * next: (value) => console.log("subject1 received", value, "from subject1"), + * return: () => console.log("subject1 returned"), + * throw: (value) => console.log("subject1 threw", value), + * }); + * subject2.subscribe({ + * signal: controller.signal, + * next: (value) => console.log("subject2 received", value, "from subject2"), + * return: () => console.log("subject2 returned"), + * throw: (value) => console.log("subject2 threw", value), + * }); + * + * subject1.next(1); // subject2 received 1 from subject1 + * subject2.next(2); // subject1 received 2 from subject2 + * subject2.return(); // subject2 returned + * subject1.next(3); // No console output since subject2 is already returned + * ``` + */ + new (name: string): BroadcastSubject; + new (name: string): BroadcastSubject; + readonly prototype: BroadcastSubject; +} + /** * A fixed UUID that is used to scope the name of the underlying {@linkcode BroadcastChannel}. This helps ensure that our * {@linkcode BroadcastSubject}'s only communicate with other {@linkcode BroadcastSubject}'s from this library. diff --git a/common/README.md b/common/README.md deleted file mode 100644 index 594bdfd..0000000 --- a/common/README.md +++ /dev/null @@ -1,101 +0,0 @@ -Platform-agnostic utilities for [@xan/observable-core](https://jsr.io/@xan/observable-core). - -## Example - -```ts -import { Observable, type Observer } from "@xan/observable-core"; -import { - all, - asObservable, - BehaviorSubject, - defer, - drop, - filter, - map, - of, - pipe, - switchMap, - takeUntil, - throwError, -} from "@xan/observable-common"; - -type Customer = Readonly>; -type AuthState = Readonly>; - -class CustomerService implements Observable { - readonly #authState = new BehaviorSubject(null); - readonly authenticated = pipe(this.#authState, map(Boolean)); - - readonly #events = new Subject<"logged-in" | "logged-out" | "logging-out">(); - readonly events = pipe(this.#events, asObservable()); - - readonly #customer = pipe( - this.authenticated, - switchMap((authenticated) => { - if (authenticated) return throwError(new Error("Not authenticated")); - return pipe(this.#authState, switchMap(this.#get)); - }), - takeUntil(this.events.filter((event) => event === "logging-out")), - ); - - login(email: string, password: string): Observable { - return pipe( - defer(() => { - this.logout(); - return this.#login(email, password); - }), - map((state) => { - this.#authState.next(state); - this.#events.next("logged-in"); - }), - ); - } - - logout(): boolean { - const { value: state } = this.#authState; - if (state === null) return false; - this.#events.next("logging-out"); - this.#authState.next(null); - this.#events.next("logged-out"); - return true; - } - - subscribe(observer: Observer): void { - return this.#customer; - } - - #get(this: void, state: AuthState): Observable { - return new Observable(async (observer) => { - try { - const response = await fetch( - `https://api.example.com/customer/${state.id}`, - { headers: { Authorization: `Bearer ${state.jwt}` } }, - ); - observer.next(await response.json()); - observer.return(); - } catch (error) { - observer.throw(error); - } - }); - } - - #login(this: void, email: string, password: string): Observable { - return new Observable(async (observer) => { - try { - const response = await fetch(`https://api.example.com/login`, { - method: "POST", - body: JSON.stringify({ email, password }), - }); - observer.next(await response.json()); - observer.return(); - } catch (error) { - observer.throw(error); - } - }); - } -} -``` - -# Glossary And Semantics - -[@xan/observable-core](https://jsr.io/@xan/observable-core#glossary-and-semantics) diff --git a/common/all.ts b/common/all.ts deleted file mode 100644 index 3dd67fb..0000000 --- a/common/all.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { type Observable, Observer, Subject } from "@xan/observable-core"; -import { MinimumArgumentsRequiredError, noop, ParameterTypeError } from "@xan/observable-internal"; -import { defer } from "./defer.ts"; -import { empty } from "./empty.ts"; -import { of } from "./of.ts"; -import { pipe } from "./pipe.ts"; -import { tap } from "./tap.ts"; -import { map } from "./map.ts"; -import { mergeMap } from "./merge-map.ts"; -import { filter } from "./filter.ts"; -import { takeUntil } from "./take-until.ts"; - -/** - * Creates and returns an [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) whose - * [`next`](https://jsr.io/@xan/observable-core/doc/~/Observer.next)ed values are calculated from the latest - * [`next`](https://jsr.io/@xan/observable-core/doc/~/Observer.next)ed values of each of its {@linkcode sources}. - * If any of the {@linkcode sources} are {@linkcode empty}, the returned [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) - * will also be {@linkcode empty}. - */ -export function all>( - sources: Readonly<{ [Key in keyof Values]: Observable }>, -): Observable; -export function all( - // Long term, it would be nice to be able to accept an Iterable for performance and flexibility. - // This new signature would have to work in conjunction with the mapped array signature above as this - // encourages more explicit types for sources as a tuple. - sources: ReadonlyArray, -): Observable> { - if (arguments.length === 0) throw new MinimumArgumentsRequiredError(); - if (!Array.isArray(sources)) throw new ParameterTypeError(0, "Array"); - if (sources.length === 0) return empty; - return defer(() => { - let receivedFirstValueCount = 0; - const { length: expectedFirstValueCount } = sources; - const values: Array = []; - const emptySourceNotifier = new Subject(); - return pipe( - of(sources), - mergeMap((source, index) => { - let isEmpty = true; - return pipe( - source, - tap( - new Observer({ - next: processNextValue, - return: processReturn, - throw: noop, - }), - ), - ); - - function processNextValue(value: unknown): void { - if (isEmpty) receivedFirstValueCount++; - isEmpty = false; - values[index] = value; - } - - function processReturn(): void { - if (isEmpty) emptySourceNotifier.next(); - } - }), - filter(() => receivedFirstValueCount === expectedFirstValueCount), - map(() => values.slice()), - takeUntil(emptySourceNotifier), - ); - }); -} diff --git a/common/as-observable.test.ts b/common/as-observable.test.ts deleted file mode 100644 index c6aca79..0000000 --- a/common/as-observable.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Observable, Observer } from "@xan/observable-core"; -import { assertEquals, assertInstanceOf, assertStrictEquals } from "@std/assert"; -import { pipe } from "./pipe.ts"; -import { asObservable } from "./as-observable.ts"; -import { of } from "./of.ts"; - -Deno.test( - "asObservable should convert a custom observable to a proper observable", - () => { - // Arrange - const observer = new Observer(); - const subscribeCalls: Array["subscribe"]>> = []; - const custom: Observable = { - subscribe(observer) { - assertInstanceOf(observer, Observer); - subscribeCalls.push([observer]); - observer.next(1); - observer.next(2); - observer.return(); - }, - }; - - // Act - const observable = pipe(custom, asObservable()); - observable.subscribe(observer); - - // Assert - assertInstanceOf(observable, Observable); - assertEquals(subscribeCalls, [[observer]]); - }, -); - -Deno.test( - "asObservable should return the same observer if it is already a proper observer", - () => { - // Arrange - const expected = of([1, 2]); - - // Act - const actual = pipe(expected, asObservable()); - - // Assert - assertStrictEquals(actual, expected); - }, -); diff --git a/common/as-observable.ts b/common/as-observable.ts deleted file mode 100644 index 143a3ca..0000000 --- a/common/as-observable.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { MinimumArgumentsRequiredError, ParameterTypeError } from "@xan/observable-internal"; -import { isObservable, type Observable, toObservable } from "@xan/observable-core"; - -/** - * Converts a custom [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) to a proper - * [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable). If the [source](https://jsr.io/@xan/observable-core#source) is - * already an instanceof [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) (which means it has - * [`Observable.prototype`](https://jsr.io/@xan/observable-core/doc/~/ObservableConstructor.prototype) in its prototype chain), - * it's returned directly. Otherwise, a new [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) object is created - * that wraps the original [source](https://jsr.io/@xan/observable-core#source). - */ -export function asObservable(): ( - source: Observable, -) => Observable { - return function asObservableFn(source) { - if (arguments.length === 0) throw new MinimumArgumentsRequiredError(); - if (!isObservable(source)) throw new ParameterTypeError(0, "Observable"); - return toObservable(source); - }; -} diff --git a/common/async-subject-constructor.ts b/common/async-subject-constructor.ts deleted file mode 100644 index deb15a7..0000000 --- a/common/async-subject-constructor.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { AsyncSubject } from "./async-subject.ts"; - -/** - * Object interface for an {@linkcode AsyncSubject} factory. - */ -export interface AsyncSubjectConstructor { - /** - * Creates and returns an object that acts as a [`Subject`](https://jsr.io/@xan/observable-core/doc/~/Subject) that buffers the most recent - * [`next`](https://jsr.io/@xan/observable-core/doc/~/Observer.next)ed value until [`return`](https://jsr.io/@xan/observable-core/doc/~/Observer.return) is called. - * Once [`return`](https://jsr.io/@xan/observable-core/doc/~/Observer.return)ed, [`next`](https://jsr.io/@xan/observable-core/doc/~/Observer.next) will be replayed - * to late [`consumers`](https://jsr.io/@xan/observable-core/doc/~/Observable.subscribe) upon [`subscription`](https://jsr.io/@xan/observable-core/doc/~/Observable.subscribe). - * @example - * ```ts - * import { AsyncSubject } from "@xan/observable-common"; - * import { Observer } from "@xan/observable-core"; - * - * const subject = new AsyncSubject(); - * subject.next(1); - * subject.next(2); - * - * subject.subscribe(new Observer((value) => console.log(value))); - * - * subject.next(3); - * - * subject.return(); // Console output: 3 - * - * subject.subscribe(new Observer((value) => console.log(value))); // Console output: 3 - * ``` - */ - new (): AsyncSubject; - new (): AsyncSubject; - readonly prototype: AsyncSubject; -} diff --git a/common/async-subject.ts b/common/async-subject.ts deleted file mode 100644 index fd207df..0000000 --- a/common/async-subject.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { isObserver, type Observer, type Subject } from "@xan/observable-core"; -import { - InstanceofError, - MinimumArgumentsRequiredError, - ParameterTypeError, -} from "@xan/observable-internal"; -import { flat } from "./flat.ts"; -import { pipe } from "./pipe.ts"; -import { ignoreElements } from "./ignore-elements.ts"; -import { ReplaySubject } from "./replay-subject.ts"; -import type { AsyncSubjectConstructor } from "./async-subject-constructor.ts"; - -/** - * Object type that acts as a variant of [`Subject`](https://jsr.io/@xan/observable-core/doc/~/Subject). - */ -export type AsyncSubject = Subject; - -export const AsyncSubject: AsyncSubjectConstructor = class { - readonly [Symbol.toStringTag] = "AsyncSubject"; - readonly #subject = new ReplaySubject(1); - readonly signal = this.#subject.signal; - readonly #observable = flat([ - pipe(this.#subject, ignoreElements()), - this.#subject, - ]); - - constructor() { - Object.freeze(this); - } - - next(value: unknown): void { - if (this instanceof AsyncSubject) this.#subject.next(value); - else throw new InstanceofError("this", "AsyncSubject"); - } - - return(): void { - if (this instanceof AsyncSubject) this.#subject.return(); - else throw new InstanceofError("this", "AsyncSubject"); - } - - throw(value: unknown): void { - if (this instanceof AsyncSubject) { - this.#subject.next(undefined); - this.#subject.throw(value); - } else throw new InstanceofError("this", "AsyncSubject"); - } - - subscribe(observer: Observer): void { - if (!(this instanceof AsyncSubject)) { - throw new InstanceofError("this", "AsyncSubject"); - } - if (arguments.length === 0) throw new MinimumArgumentsRequiredError(); - if (!isObserver(observer)) throw new ParameterTypeError(0, "Observer"); - this.#observable.subscribe(observer); - } -}; - -Object.freeze(AsyncSubject); -Object.freeze(AsyncSubject.prototype); diff --git a/common/behavior-subject-constructor.ts b/common/behavior-subject-constructor.ts deleted file mode 100644 index 9efecc2..0000000 --- a/common/behavior-subject-constructor.ts +++ /dev/null @@ -1,52 +0,0 @@ -import type { BehaviorSubject } from "./behavior-subject.ts"; - -/** - * Object interface for a {@linkcode BehaviorSubject} factory. - */ -export interface BehaviorSubjectConstructor { - /** - * Creates and returns an object that acts as a [`Subject`](https://jsr.io/@xan/observable-core/doc/~/Subject) that keeps track of it's current - * value and replays it to [`consumers`](https://jsr.io/@xan/observable-core#consumer) upon - * [`subscription`](https://jsr.io/@xan/observable-core/doc/~/Observable.subscribe). - * @example - * ```ts - * import { BehaviorSubject } from "@xan/observable-common"; - * - * const subject = new BehaviorSubject(0); - * const controller = new AbortController(); - * - * subject.subscribe({ - * signal: controller.signal, - * next: (value) => console.log(value), - * return: () => console.log("return"), - * throw: () => console.error("throw"), - * }); - * - * // console output: - * // 0 - * - * subject.next(1); - * - * // console output: - * // 1 - * - * subject.return(); - * - * // console output: - * // return - * - * subject.subscribe({ - * signal: controller.signal, - * next: (value) => console.log(value), - * return: () => console.log("return"), - * throw: () => console.error("throw"), - * }); - * - * // console output: - * // 1 - * // return - * ``` - */ - new (value: Value): BehaviorSubject; - readonly prototype: BehaviorSubject; -} diff --git a/common/defer.test.ts b/common/defer.test.ts deleted file mode 100644 index 5df7cde..0000000 --- a/common/defer.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { assertEquals } from "@std/assert"; -import { Observable, Observer } from "@xan/observable-core"; -import { dematerialize } from "./dematerialize.ts"; -import { materialize } from "./materialize.ts"; -import type { ObserverNotification } from "./observer-notification.ts"; -import { pipe } from "./pipe.ts"; -import { defer } from "./defer.ts"; - -Deno.test( - "defer should create an Observable that calls a factory to make an Observable for each new Observer", - () => { - // Arrange - const notifications: Array> = []; - const source = defer>( - () => - new Observable>((observer) => { - for (const value of [1, 2, 3]) { - observer.next(["next", value]); - if (observer.signal.aborted) return; - } - observer.return(); - }), - ); - const materialized = pipe(source, dematerialize(), materialize()); - - // Act - materialized.subscribe( - new Observer((notification) => notifications.push(notification)), - ); - - // Assert - assertEquals(notifications, [ - ["next", 1], - ["next", 2], - ["next", 3], - ["return"], - ]); - }, -); - -Deno.test("defer should throw an error if the factory throws an error", () => { - // Arrange - const error = new Error(Math.random().toString()); - const notifications: Array = []; - const source = defer(() => { - throw error; - }); - const materialized = pipe(source, materialize()); - - // Act - materialized.subscribe( - new Observer((notification) => notifications.push(notification)), - ); - - // Assert - assertEquals(notifications, [["throw", error]]); -}); diff --git a/common/defer.ts b/common/defer.ts deleted file mode 100644 index 30513da..0000000 --- a/common/defer.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Observable } from "@xan/observable-core"; -import { MinimumArgumentsRequiredError, ParameterTypeError } from "@xan/observable-internal"; -import { pipe } from "./pipe.ts"; -import { asObservable } from "./as-observable.ts"; - -/** - * Creates an [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) that, on - * [`subscribe`](https://jsr.io/@xan/observable-core/doc/~/Observable.subscribe), calls an - * [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) {@linkcode factory} to - * get an [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) for each - * [`Observer`](https://jsr.io/@xan/observable-core/doc/~/Observer). - */ -export function defer( - factory: () => Observable, -): Observable { - if (arguments.length === 0) throw new MinimumArgumentsRequiredError(); - if (typeof factory !== "function") { - throw new ParameterTypeError(0, "Function"); - } - return new Observable((observer) => pipe(factory(), asObservable()).subscribe(observer)); -} diff --git a/common/dematerialize.test.ts b/common/dematerialize.test.ts deleted file mode 100644 index 8326ee2..0000000 --- a/common/dematerialize.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { assertEquals } from "@std/assert"; -import { Observable, Observer } from "@xan/observable-core"; -import { pipe } from "./pipe.ts"; -import { dematerialize } from "./dematerialize.ts"; -import { materialize } from "./materialize.ts"; -import type { ObserverNotification } from "./observer-notification.ts"; - -Deno.test( - "dematerialize should convert a source Observable of ObserverNotification objects into a source Observable of the original values", - () => { - // Arrange - const source = new Observable>((observer) => { - for (const value of [1, 2, 3]) { - observer.next(["next", value]); - if (observer.signal.aborted) return; - } - observer.return(); - }); - const notifications: Array> = []; - const materialized = pipe(source, dematerialize(), materialize()); - - // Act - materialized.subscribe( - new Observer((notification) => notifications.push(notification)), - ); - - // Assert - assertEquals(notifications, [ - ["next", 1], - ["next", 2], - ["next", 3], - ["return"], - ]); - }, -); - -Deno.test("dematerialize should honor unsubscribe", () => { - // Arrange - const controller = new AbortController(); - const source = new Observable>((observer) => { - for (const value of [1, 2, 3]) { - observer.next(["next", value]); - if (observer.signal.aborted) return; - } - observer.next(["return"]); - observer.return(); - }); - - const notifications: Array> = []; - const materialized = pipe(source, dematerialize(), materialize()); - - // Act - materialized.subscribe( - new Observer({ - signal: controller.signal, - next: (notification) => { - notifications.push(notification); - if (notifications.length === 2) controller.abort(); - }, - }), - ); - - // Assert - assertEquals(notifications, [ - ["next", 1], - ["next", 2], - ]); -}); diff --git a/common/dematerialize.ts b/common/dematerialize.ts deleted file mode 100644 index eaab4ac..0000000 --- a/common/dematerialize.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { isObservable, Observable } from "@xan/observable-core"; -import { MinimumArgumentsRequiredError, ParameterTypeError } from "@xan/observable-internal"; -import type { ObserverNotification } from "./observer-notification.ts"; -import { pipe } from "./pipe.ts"; -import { asObservable } from "./as-observable.ts"; - -/** - * Converts an [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) of - * {@linkcode ObserverNotification|notification} objects into the emissions - * that they represent. - */ -export function dematerialize(): ( - source: Observable>, -) => Observable { - return function dematerializeFn(source) { - if (arguments.length === 0) throw new MinimumArgumentsRequiredError(); - if (!isObservable(source)) throw new ParameterTypeError(0, "Observable"); - source = pipe(source, asObservable()); - return new Observable((observer) => - source.subscribe({ - signal: observer.signal, - next(value) { - switch (value[0]) { - case "next": - observer.next(value[1]); - break; - case "return": - observer.return(); - break; - case "throw": - observer.throw(value[1]); - break; - default: - observer.throw(new ParameterTypeError(0, "ObserverNotification")); - break; - } - }, - return: () => observer.return(), - throw: (value) => observer.throw(value), - }) - ); - }; -} diff --git a/common/deno.json b/common/deno.json deleted file mode 100644 index 6d8c24c..0000000 --- a/common/deno.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "@xan/observable-common", - "version": "0.8.0", - "license": "MIT", - "exports": "./mod.ts" -} diff --git a/common/empty.ts b/common/empty.ts deleted file mode 100644 index f511c48..0000000 --- a/common/empty.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { Observable } from "@xan/observable-core"; -import { of } from "./of.ts"; - -/** - * An [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) that calls [`return`](https://jsr.io/@xan/observable-core/doc/~/Observer.return) - * immediately on [`subscribe`](https://jsr.io/@xan/observable-core/doc/~/Observable.subscribe). - * @example - * ```ts - * import { empty } from "@xan/observable-common"; - * - * const controller = new AbortController(); - * - * empty.subscribe({ - * signal: controller.signal, - * next: () => console.log("next"), - * throw: () => console.log("throw"), - * return: () => console.log("return"), - * }); - * - * // Console output: - * // return - * ``` - */ -export const empty: Observable = of([]); diff --git a/common/merge.ts b/common/merge.ts deleted file mode 100644 index c2273c2..0000000 --- a/common/merge.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { Observable } from "@xan/observable-core"; -import { - identity, - isIterable, - MinimumArgumentsRequiredError, - ParameterTypeError, -} from "@xan/observable-internal"; -import { of } from "./of.ts"; -import { pipe } from "./pipe.ts"; -import { mergeMap } from "./merge-map.ts"; - -/** - * Creates and returns an [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) which concurrently - * [`next`](https://jsr.io/@xan/observable-core/doc/~/Observer.next)s all values from every given {@linkcode sources|source}. - */ -export function merge( - // Accepting an Iterable is a design choice for performance (iterables are lazily evaluated) and - // flexibility (can accept any iterable, not just arrays). - sources: Iterable>, -): Observable { - if (arguments.length === 0) throw new MinimumArgumentsRequiredError(); - if (!isIterable(sources)) throw new ParameterTypeError(0, "Iterable"); - return pipe(of(sources), mergeMap(identity)); -} diff --git a/common/mod.test.ts b/common/mod.test.ts deleted file mode 100644 index 447c3b9..0000000 --- a/common/mod.test.ts +++ /dev/null @@ -1,4 +0,0 @@ -Deno.test("mod should be importable", async () => { - // Arrange / Act / Assert - await import("./mod.ts"); -}); diff --git a/common/mod.ts b/common/mod.ts deleted file mode 100644 index 2652d72..0000000 --- a/common/mod.ts +++ /dev/null @@ -1,33 +0,0 @@ -export { all } from "./all.ts"; -export { asObservable } from "./as-observable.ts"; -export type { AsyncSubjectConstructor } from "./async-subject-constructor.ts"; -export { AsyncSubject } from "./async-subject.ts"; -export type { BehaviorSubjectConstructor } from "./behavior-subject-constructor.ts"; -export { BehaviorSubject } from "./behavior-subject.ts"; -export { defer } from "./defer.ts"; -export { dematerialize } from "./dematerialize.ts"; -export { drop } from "./drop.ts"; -export { empty } from "./empty.ts"; -export { exhaustMap } from "./exhaust-map.ts"; -export { filter } from "./filter.ts"; -export { finalize } from "./finalize.ts"; -export { flatMap } from "./flat-map.ts"; -export { flat } from "./flat.ts"; -export { ignoreElements } from "./ignore-elements.ts"; -export { map } from "./map.ts"; -export { materialize } from "./materialize.ts"; -export { mergeMap } from "./merge-map.ts"; -export { merge } from "./merge.ts"; -export { never } from "./never.ts"; -export type { ObserverNotification } from "./observer-notification.ts"; -export { of } from "./of.ts"; -export { pipe } from "./pipe.ts"; -export { race } from "./race.ts"; -export type { ReplaySubjectConstructor } from "./replay-subject-constructor.ts"; -export { ReplaySubject } from "./replay-subject.ts"; -export { switchMap } from "./switch-map.ts"; -export { takeUntil } from "./take-until.ts"; -export { take } from "./take.ts"; -export { tap } from "./tap.ts"; -export { throwError } from "./throw-error.ts"; -export { timer } from "./timer.ts"; diff --git a/common/never.ts b/common/never.ts deleted file mode 100644 index e456504..0000000 --- a/common/never.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Observable } from "@xan/observable-core"; -import { noop } from "@xan/observable-internal"; - -/** - * An [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) that does nothing on - * [`subscribe`](https://jsr.io/@xan/observable-core/doc/~/Observable.subscribe). - * @example - * ```ts - * import { never } from "@xan/observable-common"; - * - * const controller = new AbortController(); - * - * never.subscribe({ - * signal: controller.signal, - * next: () => console.log("next"), // Never called - * throw: () => console.log("throw"), // Never called - * return: () => console.log("return"), // Never called - * }); - * ``` - */ -export const never: Observable = new Observable(noop); diff --git a/common/observer-notification.ts b/common/observer-notification.ts deleted file mode 100644 index cda5012..0000000 --- a/common/observer-notification.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Observer } from "@xan/observable-core"; - -/** - * Represents any type of [`Observer`](https://jsr.io/@xan/observable-core/doc/~/Observer) notification - * ([`next`](https://jsr.io/@xan/observable-core/doc/~/Observer.next), - * [`return`](https://jsr.io/@xan/observable-core/doc/~/Observer.return), or - * [`throw`](https://jsr.io/@xan/observable-core/doc/~/Observer.throw)). - */ -export type ObserverNotification = Readonly< - | [type: Extract<"next", keyof Observer>, value: Value] - | [type: Extract<"return", keyof Observer>] - | [type: Extract<"throw", keyof Observer>, value: unknown] ->; diff --git a/common/race.ts b/common/race.ts deleted file mode 100644 index 52fa8e3..0000000 --- a/common/race.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { type Observable, Observer } from "@xan/observable-core"; -import { - isIterable, - MinimumArgumentsRequiredError, - noop, - ParameterTypeError, -} from "@xan/observable-internal"; -import { defer } from "./defer.ts"; -import { of } from "./of.ts"; -import { pipe } from "./pipe.ts"; -import { tap } from "./tap.ts"; -import { mergeMap } from "./merge-map.ts"; -import { takeUntil } from "./take-until.ts"; -import { filter } from "./filter.ts"; -import { AsyncSubject } from "./async-subject.ts"; - -/** - * Creates and returns an [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) that mirrors the first {@linkcode sources|source} - * [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) to [`next`](https://jsr.io/@xan/observable-core/doc/~/Observer.next) or - * [`throw`](https://jsr.io/@xan/observable-core/doc/~/Observer.throw) a value. - */ -export function race( - // Accepting an Iterable is a design choice for performance (iterables are lazily evaluated) and - // flexibility (can accept any iterable, not just arrays). - sources: Iterable>, -): Observable { - if (arguments.length === 0) throw new MinimumArgumentsRequiredError(); - if (!isIterable(sources)) throw new ParameterTypeError(0, "Iterable"); - return defer(() => { - const finished = new AsyncSubject(); - return pipe( - of(sources), - takeUntil(finished), - mergeMap((source, index) => { - const observer = new Observer({ next: finish, throw: noop }); - const lost = pipe(finished, filter(isLoser)); - return pipe(source, tap(observer), takeUntil(lost)); - - function finish(): void { - finished.next(index); - finished.return(); - } - - function isLoser(winnerIndex: number): boolean { - return winnerIndex !== index; - } - }), - ); - }); -} diff --git a/common/replay-subject-constructor.ts b/common/replay-subject-constructor.ts deleted file mode 100644 index d875f8b..0000000 --- a/common/replay-subject-constructor.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { ReplaySubject } from "./replay-subject.ts"; - -/** - * Object interface for an {@linkcode ReplaySubject} factory. - */ -export interface ReplaySubjectConstructor { - /** - * Creates and returns an object that acts as a [`Subject`](https://jsr.io/@xan/observable-core/doc/~/Subject) that replays - * buffered [`next`](https://jsr.io/@xan/observable-core/doc/~/Observer.next)ed values upon - * [`subscription`](https://jsr.io/@xan/observable-core/doc/~/Observable.subscribe). - * @example - * ```ts - * import { ReplaySubject } from "@xan/observable-common"; - * - * const subject = new ReplaySubject(3); - * const controller = new AbortController(); - * - * subject.next(1); // Stored in buffer - * subject.next(2); // Stored in buffer - * subject.next(3); // Stored in buffer - * subject.next(4); // Stored in buffer and 1 gets trimmed off - * - * subject.subscribe({ - * signal: controller.signal, - * next: (value) => console.log(value), - * return: () => console.log("return"), - * throw: (value) => console.log("throw", value), - * }); - * - * // Console output: - * // 2 - * // 3 - * // 4 - * - * // Values pushed after the subscribe will emit immediately - * // unless the subject is already finalized. - * subject.next(5); // Stored in buffer and 2 gets trimmed off - * - * // Console output: - * // 5 - * - * subject.subscribe({ - * signal: controller.signal, - * next: (value) => console.log(value), - * return: () => console.log("return"), - * throw: (value) => console.log("throw", value), - * }); - * - * // Console output: - * // 3 - * // 4 - * // 5 - * ``` - */ - new (bufferSize: number): ReplaySubject; - readonly prototype: ReplaySubject; -} diff --git a/common/switch-map.ts b/common/switch-map.ts deleted file mode 100644 index 006657a..0000000 --- a/common/switch-map.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { isObservable, type Observable, Observer, Subject } from "@xan/observable-core"; -import { MinimumArgumentsRequiredError, noop, ParameterTypeError } from "@xan/observable-internal"; -import { defer } from "./defer.ts"; -import { pipe } from "./pipe.ts"; -import { takeUntil } from "./take-until.ts"; -import { tap } from "./tap.ts"; -import { mergeMap } from "./merge-map.ts"; -import { asObservable } from "./as-observable.ts"; - -/** - * {@linkcode project|Projects} each source value to an [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) which is - * merged in the output [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable), emitting values only from the most - * recently {@linkcode project|projected} [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable). - */ -export function switchMap( - project: (value: In, index: number) => Observable, -): (source: Observable) => Observable { - if (arguments.length === 0) throw new MinimumArgumentsRequiredError(); - if (typeof project !== "function") { - throw new ParameterTypeError(0, "Function"); - } - return function switchMapFn(source) { - if (arguments.length === 0) throw new MinimumArgumentsRequiredError(); - if (!isObservable(source)) throw new ParameterTypeError(0, "Observable"); - source = pipe(source, asObservable()); - return defer(() => { - const switching = new Subject(); - return pipe( - source, - tap(new Observer({ next: () => switching.next(), throw: noop })), - mergeMap((value, index) => pipe(project(value, index), takeUntil(switching))), - ); - }); - }; -} diff --git a/common/throw-error.ts b/common/throw-error.ts deleted file mode 100644 index 533a7a6..0000000 --- a/common/throw-error.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Observable } from "@xan/observable-core"; - -/** - * Creates an [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) that will [`throw`](https://jsr.io/@xan/observable-core/doc/~/Observer.throw) the - * given `value` immediately upon [`subscribe`](https://jsr.io/@xan/observable-core/doc/~/Observable.subscribe). - * - * @param value The value to [`throw`](https://jsr.io/@xan/observable-core/doc/~/Observer.throw). - * @example - * ```ts - * import { throwError } from "@xan/observable-common"; - * - * const controller = new AbortController(); - * - * throwError(new Error("throw")).subscribe({ - * signal: controller.signal, - * next: (value) => console.log(value), // Never called - * return: () => console.log("return"), // Never called - * throw: (value) => console.log(value), // Called immediately - * }); - * ``` - */ -export function throwError(value: unknown): Observable { - return new Observable((observer) => observer.throw(value)); -} diff --git a/core/README.md b/core/README.md index 1e4334a..4ca4768 100644 --- a/core/README.md +++ b/core/README.md @@ -1,10 +1,10 @@ -# @xan/observable-core +# @observable/core A lightweight, [RxJS](https://rxjs.dev/)-inspired implementation of the [Observer pattern](https://refactoring.guru/design-patterns/observer) in JavaScript. Features -[`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable)'s with +[`Observable`](https://jsr.io/@observable/core/doc/~/Observable)'s with [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController)-based -[`unsubscription`](https://jsr.io/@xan/observable-core/doc/~/Observer.signal), supporting both +[`unsubscription`](https://jsr.io/@observable/core/doc/~/Observer.signal), supporting both synchronous and asynchronous [producers](#producer). ## Build @@ -23,7 +23,7 @@ Run `deno task test` or `deno task test:ci` to execute the unit tests via ## Example ```ts -import { Observable } from "@xan/observable-core"; +import { Observable } from "@observable/core"; const observable = new Observable<0>((observer) => { // Create a timeout as our producer to next a successful execution code (0) after 1 second. @@ -42,11 +42,11 @@ const observable = new Observable<0>((observer) => { # Glossary And Semantics -When discussing and documenting [`Observable`](https://jsr.io/@xan/observable-core/~/Observable)s, -it's important to have a common language and a known set of rules around what is going on. This -document is an attempt to standardize these things so we can try to control the language in our -docs, and hopefully other publications about this library, so we can discuss reactive programming -with this library on consistent terms. +When discussing and documenting [`Observable`](https://jsr.io/@observable/core/~/Observable)s, it's +important to have a common language and a known set of rules around what is going on. This document +is an attempt to standardize these things so we can try to control the language in our docs, and +hopefully other publications about this library, so we can discuss reactive programming with this +library on consistent terms. While not all of the documentation for this library reflects this terminology, it's a goal to ensure it does, and to ensure the language and names around the library use this document as a source of @@ -56,7 +56,7 @@ truth and unified language. There are high level entities that are frequently discussed. It's important to define them separately from other lower-level concepts, because they relate to the nature of -[`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable). +[`Observable`](https://jsr.io/@observable/core/doc/~/Observable). ### Consumer @@ -87,8 +87,8 @@ A [consumer](#consumer) reacting to [producer](#producer) [notifications](#notif ### Observation Chain -When an [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) uses another -[`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) as a [producer](#producer), an +When an [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) uses another +[`Observable`](https://jsr.io/@observable/core/doc/~/Observable) as a [producer](#producer), an "observation chain" is set up. That is a chain of [observation](#observation) such that multiple [observers](#observation) are notifying each other in a unidirectional way toward the final [consumer](#consumer). @@ -96,10 +96,10 @@ When an [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) use ### Notification The act of a [producer](#producer) pushing -[`next`](https://jsr.io/@xan/observable-core/doc/~/Observer.next)ed values, -[`throw`](https://jsr.io/@xan/observable-core/doc/~/Observer.throw)n values, or -[`return`](https://jsr.io/@xan/observable-core/doc/~/Observer.return)s to a [consumer](#consumer) to -be [observed](#observation). +[`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values, +[`throw`](https://jsr.io/@observable/core/doc/~/Observer.throw)n values, or +[`return`](https://jsr.io/@observable/core/doc/~/Observer.return)s to a [consumer](#consumer) to be +[observed](#observation). ## Major Concepts @@ -108,29 +108,28 @@ in [push-based](#push) reactive systems. ### Cold -An [`Observable`](https://jsr.io/@xan/observable-core/~/Observable) is "cold" when it creates a new -[producer](#producer) during -[`subscribe`](https://jsr.io/@xan/observable-core/~/Observable.subscribe) for every new -[subscription](#subscription). As a result, "cold" -[`Observable`](https://jsr.io/@xan/observable-core/~/Observable)s are _always_ [unicast](#unicast), +An [`Observable`](https://jsr.io/@observable/core/~/Observable) is "cold" when it creates a new +[producer](#producer) during [`subscribe`](https://jsr.io/@observable/core/~/Observable.subscribe) +for every new [subscription](#subscription). As a result, "cold" +[`Observable`](https://jsr.io/@observable/core/~/Observable)s are _always_ [unicast](#unicast), being one [producer](#producer) [observed](#observation) by one [consumer](#consumer). Cold -[`Observable`](https://jsr.io/@xan/observable-core/~/Observable)s can be made [hot](#hot) but not -the other way around. +[`Observable`](https://jsr.io/@observable/core/~/Observable)s can be made [hot](#hot) but not the +other way around. ### Hot -An [`Observable`](https://jsr.io/@xan/observable-core/~/Observable) is "hot", when its +An [`Observable`](https://jsr.io/@observable/core/~/Observable) is "hot", when its [producer](#producer) was created outside of the context of the -[`subscribe`](https://jsr.io/@xan/observable-core/~/Observable.subscribe) action. This means that -the "hot" [`Observable`](https://jsr.io/@xan/observable-core/~/Observable) is almost always +[`subscribe`](https://jsr.io/@observable/core/~/Observable.subscribe) action. This means that the +"hot" [`Observable`](https://jsr.io/@observable/core/~/Observable) is almost always [multicast](#multicast). It is possible that a "hot" -[`Observable`](https://jsr.io/@xan/observable-core/~/Observable) is still _technically_ +[`Observable`](https://jsr.io/@observable/core/~/Observable) is still _technically_ [unicast](#unicast), if it is engineered to only allow one [subscription](#subscription) at a time, however, there is no straightforward mechanism for this in the library, and the scenario is an unlikely one. For the purposes of discussion, all "hot" -[`Observable`](https://jsr.io/@xan/observable-core/~/Observable)s can be assumed to be -[multicast](#multicast). Hot [`Observable`](https://jsr.io/@xan/observable-core/~/Observable)s -cannot be made [cold](#cold). +[`Observable`](https://jsr.io/@observable/core/~/Observable)s can be assumed to be +[multicast](#multicast). Hot [`Observable`](https://jsr.io/@observable/core/~/Observable)s cannot be +made [cold](#cold). ### Multicast @@ -144,10 +143,10 @@ The act of one [producer](#producer) being [observed](#observation) by **only on ### Push -[`Observer`](https://jsr.io/@xan/observable-core/doc/~/Observer)s are a push-based type. That means +[`Observer`](https://jsr.io/@observable/core/doc/~/Observer)s are a push-based type. That means rather than having the [consumer](#consumer) call a function or perform some other action to get a value, the [consumer](#consumer) receives values as soon as the [producer](#producer) has produced -them, via a registered [next](https://jsr.io/@xan/observable-core/doc/~/Observer.next) handler. +them, via a registered [next](https://jsr.io/@observable/core/doc/~/Observer.next) handler. ### Pull @@ -168,22 +167,22 @@ A factory function that creates an [operator function](#operator-function). ### Operator Function -A function that takes an [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable), and -maps it to a new [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable). Nothing more, -nothing less. [Operator functions](#operator-function) are created by [operators](#operator). +A function that takes an [`Observable`](https://jsr.io/@observable/core/doc/~/Observable), and maps +it to a new [`Observable`](https://jsr.io/@observable/core/doc/~/Observable). Nothing more, nothing +less. [Operator functions](#operator-function) are created by [operators](#operator). ### Operation An action taken while handling a [notification](#notification), as set up by an [operator](#operator) and/or [operator function](#operator-function). During [subscription](#subscription) to that -[`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable), [operations](#operation) are +[`Observable`](https://jsr.io/@observable/core/doc/~/Observable), [operations](#operation) are performed in an order dictated by the [observation chain](#observation-chain). ### Stream A "stream" or "streaming" in the case of -[`observables`](https://jsr.io/@xan/observable-core/doc/~/Observable), refers to the collection of +[`observables`](https://jsr.io/@observable/core/doc/~/Observable), refers to the collection of [operations](#operation), as they are processed during a [subscription](#subscription). This is not to be confused with node Streams, and the word "stream", on its own, should be used sparingly in documentation and articles. Instead, prefer [observation chain](#observation-chain), @@ -192,32 +191,32 @@ fine to use given this defined meaning. ### Source -An [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) that will supply values to -another [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable). This -[source](#source), will be the [producer](#producer) for the resulting -[`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) and all of its +An [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) that will supply values to +another [`Observable`](https://jsr.io/@observable/core/doc/~/Observable). This [source](#source), +will be the [producer](#producer) for the resulting +[`Observable`](https://jsr.io/@observable/core/doc/~/Observable) and all of its [subscriptions](#subscriptions). Sources may generally be any type of -[`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable). +[`Observable`](https://jsr.io/@observable/core/doc/~/Observable). ### Notifier -An [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) that is being used to notify -another [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) that it needs to -perform some action. The action should only occur on a -[`next`](https://jsr.io/@xan/observable-core/doc/~/Observer.next) and _never_ on -[`return`](https://jsr.io/@xan/observable-core/doc/~/Observer.return) or -[`throw`](https://jsr.io/@xan/observable-core/doc/~/Observer.throw). +An [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) that is being used to notify +another [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) that it needs to perform +some action. The action should only occur on a +[`next`](https://jsr.io/@observable/core/doc/~/Observer.next) and _never_ on +[`return`](https://jsr.io/@observable/core/doc/~/Observer.return) or +[`throw`](https://jsr.io/@observable/core/doc/~/Observer.throw). ## Other Concepts ### Unhandled Errors -An "unhandled error" is any [`throw`](https://jsr.io/@xan/observable-core/doc/~/Observer.throw)n -value that is not handled by a [consumer](#consumer)-provided function, which is generally provided -during the [`subscribe`](https://jsr.io/@xan/observable-core/doc/~/Observable.subscribe) action by -constructing a new [`Observer`](https://jsr.io/@xan/observable-core/doc/~/Observer). If no -[`throw handler`](https://jsr.io/@xan/observable-core/doc/~/Observer.throw) was provided, this -library will assume the error is "unhandled" and rethrow it on a new callstack to prevent +An "unhandled error" is any [`throw`](https://jsr.io/@observable/core/doc/~/Observer.throw)n value +that is not handled by a [consumer](#consumer)-provided function, which is generally provided during +the [`subscribe`](https://jsr.io/@observable/core/doc/~/Observable.subscribe) action by constructing +a new [`Observer`](https://jsr.io/@observable/core/doc/~/Observer). If no +[`throw handler`](https://jsr.io/@observable/core/doc/~/Observer.throw) was provided, this library +will assume the error is "unhandled" and rethrow it on a new callstack to prevent ["producer interference"](#producer-interference). ### Producer Interference diff --git a/core/deno.json b/core/deno.json index 7a087ca..65eeb4a 100644 --- a/core/deno.json +++ b/core/deno.json @@ -1,6 +1,6 @@ { - "name": "@xan/observable-core", - "version": "0.7.0", + "name": "@observable/core", + "version": "0.1.0", "license": "MIT", "exports": "./mod.ts" } diff --git a/core/is-observable.test.ts b/core/is-observable.test.ts index e1e31b5..fd7afc0 100644 --- a/core/is-observable.test.ts +++ b/core/is-observable.test.ts @@ -1,5 +1,5 @@ import { assertStrictEquals } from "@std/assert"; -import { noop } from "@xan/observable-internal"; +import { noop } from "@observable/internal"; import { isObservable } from "./is-observable.ts"; import { Observable } from "./observable.ts"; diff --git a/core/is-observable.ts b/core/is-observable.ts index e05b258..5be8004 100644 --- a/core/is-observable.ts +++ b/core/is-observable.ts @@ -1,11 +1,11 @@ -import { isObject, MinimumArgumentsRequiredError } from "@xan/observable-internal"; +import { isObject, MinimumArgumentsRequiredError } from "@observable/internal"; import { Observable } from "./observable.ts"; /** * Checks if a {@linkcode value} is an object that implements the {@linkcode Observable} interface. * @example * ```ts - * import { isObservable, Observable } from "@xan/observable-core"; + * import { isObservable, Observable } from "@observable/core"; * * const observableInstance = new Observable((observer) => { * // Implementation omitted for brevity. diff --git a/core/is-observer.test.ts b/core/is-observer.test.ts index 68f329b..3a8555d 100644 --- a/core/is-observer.test.ts +++ b/core/is-observer.test.ts @@ -1,7 +1,7 @@ import { assertStrictEquals } from "@std/assert"; import { isObserver } from "./is-observer.ts"; import type { Observer } from "./observer.ts"; -import { noop } from "@xan/observable-internal"; +import { noop } from "@observable/internal"; Deno.test("isObserver should return false if the value is null", () => { // Arrange diff --git a/core/is-observer.ts b/core/is-observer.ts index e40faa3..d0f1d49 100644 --- a/core/is-observer.ts +++ b/core/is-observer.ts @@ -1,11 +1,11 @@ -import { isAbortSignal, isObject, MinimumArgumentsRequiredError } from "@xan/observable-internal"; +import { isAbortSignal, isObject, MinimumArgumentsRequiredError } from "@observable/internal"; import { Observer } from "./observer.ts"; /** * Checks if a {@linkcode value} is an object that implements the {@linkcode Observer} interface. * @example * ```ts - * import { isObserver, Observer } from "@xan/observable-core"; + * import { isObserver, Observer } from "@observable/core"; * * const instance = new Observer((value) => { * // Implementation omitted for brevity. diff --git a/core/is-subject.test.ts b/core/is-subject.test.ts index 93b0fde..5acefe5 100644 --- a/core/is-subject.test.ts +++ b/core/is-subject.test.ts @@ -1,7 +1,7 @@ import { assertEquals } from "@std/assert"; import { isSubject } from "./is-subject.ts"; import { Subject } from "./subject.ts"; -import { noop } from "@xan/observable-internal"; +import { noop } from "@observable/internal"; Deno.test( "isSubject should return true if the value is an instance of Subject", diff --git a/core/is-subject.ts b/core/is-subject.ts index c03d7e2..c5e748a 100644 --- a/core/is-subject.ts +++ b/core/is-subject.ts @@ -1,4 +1,4 @@ -import { MinimumArgumentsRequiredError } from "@xan/observable-internal"; +import { MinimumArgumentsRequiredError } from "@observable/internal"; import { Subject } from "./subject.ts"; import { isObservable } from "./is-observable.ts"; import { isObserver } from "./is-observer.ts"; @@ -7,7 +7,7 @@ import { isObserver } from "./is-observer.ts"; * Checks if a {@linkcode value} is an object that implements the {@linkcode Subject} interface. * @example * ```ts - * import { isSubject, Subject } from "@xan/observable-core"; + * import { isSubject, Subject } from "@observable/core"; * * const subjectInstance = new Subject(); * isSubject(subjectInstance); // true diff --git a/core/observable-constructor.ts b/core/observable-constructor.ts index 9c23558..6ef9cdc 100644 --- a/core/observable-constructor.ts +++ b/core/observable-constructor.ts @@ -6,13 +6,13 @@ import type { Observer } from "./observer.ts"; */ export interface ObservableConstructor { /** - * Creates and returns an object that acts as a template for connecting a [producer](https://jsr.io/@xan/observable-core#producer) - * to a [consumer](https://jsr.io/@xan/observable-core#consumer) via a {@linkcode Observable.subscribe|subscribe} action. + * Creates and returns an object that acts as a template for connecting a [producer](https://jsr.io/@observable/core#producer) + * to a [consumer](https://jsr.io/@observable/core#consumer) via a {@linkcode Observable.subscribe|subscribe} action. * @param subscribe The function called for each {@linkcode Observable.subscribe|subscribe} action. * @example * Creating an observable with a synchronous producer. * ```ts - * import { Observable } from "@xan/observable-core"; + * import { Observable } from "@observable/core"; * * const observable = new Observable((observer) => { * // Create an Array as our producer to next a sequence of values. @@ -47,7 +47,7 @@ export interface ObservableConstructor { * @example * Creating an observable with an asynchronous producer. * ```ts - * import { Observable } from "@xan/observable-core"; + * import { Observable } from "@observable/core"; * * const observable = new Observable<0>((observer) => { * // Create a timeout as our producer to next a successful execution code (0) after 1 second. @@ -84,7 +84,7 @@ export interface ObservableConstructor { * @example * Creating an observable with no producer. * ```ts - * import { Observable } from "@xan/observable-core"; + * import { Observable } from "@observable/core"; * * const observable = new Observable(); * diff --git a/core/observable.test.ts b/core/observable.test.ts index 00737aa..58bcf53 100644 --- a/core/observable.test.ts +++ b/core/observable.test.ts @@ -1,7 +1,7 @@ import { assertEquals, assertInstanceOf, assertStrictEquals, assertThrows } from "@std/assert"; import { Observer } from "./observer.ts"; import { Observable } from "./observable.ts"; -import { noop } from "@xan/observable-internal"; +import { noop } from "@observable/internal"; Deno.test("Observable.toString should be '[object Observable]'", () => { // Arrange / Act / Assert diff --git a/core/observable.ts b/core/observable.ts index 71105bd..8a83091 100644 --- a/core/observable.ts +++ b/core/observable.ts @@ -2,7 +2,7 @@ import { InstanceofError, MinimumArgumentsRequiredError, ParameterTypeError, -} from "@xan/observable-internal"; +} from "@observable/internal"; import { isObserver } from "./is-observer.ts"; import type { Observer } from "./observer.ts"; import { toObserver } from "./to-observer.ts"; @@ -10,14 +10,14 @@ import type { ObservableConstructor } from "./observable-constructor.ts"; /** * Object interface that acts as a template for connecting an {@linkcode Observer}, as a - * [consumer](https://jsr.io/@xan/observable-core#consumer), to a [producer](https://jsr.io/@xan/observable-core#producer), + * [consumer](https://jsr.io/@observable/core#consumer), to a [producer](https://jsr.io/@observable/core#producer), * via a {@linkcode Observable.subscribe|subscribe} action. */ export interface Observable { /** - * The act of a [consumer](https://jsr.io/@xan/observable-core#consumer) requesting from an - * {@linkcode Observable} to set up a [`subscription`](https://jsr.io/@xan/observable-core#subscription) - * so that it may [`observe`](https://jsr.io/@xan/observable-core#observation) a [producer](https://jsr.io/@xan/observable-core#producer). + * The act of a [consumer](https://jsr.io/@observable/core#consumer) requesting from an + * {@linkcode Observable} to set up a [`subscription`](https://jsr.io/@observable/core#subscription) + * so that it may [`observe`](https://jsr.io/@observable/core#observation) a [producer](https://jsr.io/@observable/core#producer). */ subscribe(observer: Observer): void; } diff --git a/core/observer-constructor.ts b/core/observer-constructor.ts index acf43de..67cea04 100644 --- a/core/observer-constructor.ts +++ b/core/observer-constructor.ts @@ -5,10 +5,10 @@ import type { Observer } from "./observer.ts"; */ export interface ObserverConstructor { /** - * Creates and return a object that provides a standard way to [`consume`](https://jsr.io/@xan/observable-core#consumer) a sequence of values + * Creates and return a object that provides a standard way to [`consume`](https://jsr.io/@observable/core#consumer) a sequence of values * (either finite or infinite). * ```ts - * import { Observer } from "@xan/observable-core"; + * import { Observer } from "@observable/core"; * * const observer = new Observer<0>({ * next: (value) => console.log(value), diff --git a/core/observer.test.ts b/core/observer.test.ts index 04c8aa6..56259fc 100644 --- a/core/observer.test.ts +++ b/core/observer.test.ts @@ -1,4 +1,4 @@ -import { noop } from "@xan/observable-internal"; +import { noop } from "@observable/internal"; import { Observer } from "./observer.ts"; import { assertEquals, assertInstanceOf, assertStrictEquals, assertThrows } from "@std/assert"; import { isObserver } from "./is-observer.ts"; diff --git a/core/observer.ts b/core/observer.ts index 08c7d78..430b7f7 100644 --- a/core/observer.ts +++ b/core/observer.ts @@ -5,11 +5,11 @@ import { isObject, MinimumArgumentsRequiredError, ParameterTypeError, -} from "@xan/observable-internal"; +} from "@observable/internal"; import type { ObserverConstructor } from "./observer-constructor.ts"; /** - * Object interface that defines a standard way to [`consume`](https://jsr.io/@xan/observable-core#consumer) a + * Object interface that defines a standard way to [`consume`](https://jsr.io/@observable/core#consumer) a * sequence of values (either finite or infinite). */ // This is meant to reflect similar semantics as the Iterator protocol, while also supporting aborts. @@ -17,21 +17,21 @@ import type { ObserverConstructor } from "./observer-constructor.ts"; // is different. export interface Observer { /** - * The [consumer](https://jsr.io/@xan/observable-core#consumer) is telling the [producer](https://jsr.io/@xan/observable-core#producer) + * The [consumer](https://jsr.io/@observable/core#consumer) is telling the [producer](https://jsr.io/@observable/core#producer) * it's no longer interested in receiving {@linkcode Value|values}. */ readonly signal: AbortSignal; /** - * The [producer](https://jsr.io/@xan/observable-core#producer) is pushing a {@linkcode value} to the [consumer](https://jsr.io/@xan/observable-core#consumer). + * The [producer](https://jsr.io/@observable/core#producer) is pushing a {@linkcode value} to the [consumer](https://jsr.io/@observable/core#consumer). */ next(value: Value): void; /** - * The [producer](https://jsr.io/@xan/observable-core#producer) is telling the [consumer](https://jsr.io/@xan/observable-core#consumer) + * The [producer](https://jsr.io/@observable/core#producer) is telling the [consumer](https://jsr.io/@observable/core#consumer) * that it does not intend to {@linkcode next} any more values, and can perform any cleanup actions. */ return(): void; /** - * The [producer](https://jsr.io/@xan/observable-core#producer) is telling the [consumer](https://jsr.io/@xan/observable-core#consumer) that + * The [producer](https://jsr.io/@observable/core#producer) is telling the [consumer](https://jsr.io/@observable/core#consumer) that * it has encountered a {@linkcode value|problem}, does not intend to {@linkcode next} any more values, and can perform any cleanup actions. */ throw(value: unknown): void; diff --git a/core/subject-constructor.ts b/core/subject-constructor.ts index e9bd505..1b21c08 100644 --- a/core/subject-constructor.ts +++ b/core/subject-constructor.ts @@ -5,15 +5,15 @@ import type { Subject } from "./subject.ts"; */ export interface SubjectConstructor { /** - * Creates and returns an object that acts as both an [`observer`](https://jsr.io/@xan/observable-core/doc/~/Observer) - * ([`multicast`](https://jsr.io/@xan/observable-core#multicast)) and an [`observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) - * ([`hot`](https://jsr.io/@xan/observable-core#hot)). [`return`](https://jsr.io/@xan/observable-core/doc/~/Observer.return) - * and [`throw`](https://jsr.io/@xan/observable-core/doc/~/Observer.throw) will be replayed to late - * [`consumers`](https://jsr.io/@xan/observable-core#consumer) upon [`subscription`](https://jsr.io/@xan/observable-core/doc/~/Observable.subscribe). + * Creates and returns an object that acts as both an [`observer`](https://jsr.io/@observable/core/doc/~/Observer) + * ([`multicast`](https://jsr.io/@observable/core#multicast)) and an [`observable`](https://jsr.io/@observable/core/doc/~/Observable) + * ([`hot`](https://jsr.io/@observable/core#hot)). [`return`](https://jsr.io/@observable/core/doc/~/Observer.return) + * and [`throw`](https://jsr.io/@observable/core/doc/~/Observer.throw) will be replayed to late + * [`consumers`](https://jsr.io/@observable/core#consumer) upon [`subscription`](https://jsr.io/@observable/core/doc/~/Observable.subscribe). * @example * Basic * ```ts - * import { Subject } from "@xan/observable-core"; + * import { Subject } from "@observable/core"; * * const subject = new Subject(); * const controller = new AbortController(); @@ -61,7 +61,7 @@ export interface SubjectConstructor { * @example * Advanced * ```ts - * import { Subject, toObservable } from "@xan/observable-core"; + * import { Subject, toObservable } from "@observable/core"; * * class Authenticator { * readonly #events = new Subject(); diff --git a/core/subject.ts b/core/subject.ts index e874a5c..3d1e4ec 100644 --- a/core/subject.ts +++ b/core/subject.ts @@ -5,7 +5,7 @@ import { InstanceofError, MinimumArgumentsRequiredError, ParameterTypeError, -} from "@xan/observable-internal"; +} from "@observable/internal"; import type { SubjectConstructor } from "./subject-constructor.ts"; /** @@ -22,7 +22,7 @@ const notThrown = Symbol("Flag indicating that a value is not thrown."); export const Subject: SubjectConstructor = class { readonly [Symbol.toStringTag] = "Subject"; /** - * Tracking the value that was thrown by the [producer](https://jsr.io/@xan/observable-core#producer), if any. + * Tracking the value that was thrown by the [producer](https://jsr.io/@observable/core#producer), if any. */ #thrown: unknown = notThrown; diff --git a/core/to-observable.test.ts b/core/to-observable.test.ts index 3158d3b..81d7d94 100644 --- a/core/to-observable.test.ts +++ b/core/to-observable.test.ts @@ -1,4 +1,4 @@ -import { Observable, Observer } from "@xan/observable-core"; +import { Observable, Observer } from "@observable/core"; import { assertEquals, assertInstanceOf, assertStrictEquals } from "@std/assert"; import { toObservable } from "./to-observable.ts"; diff --git a/core/to-observable.ts b/core/to-observable.ts index f8f9343..d5af9b2 100644 --- a/core/to-observable.ts +++ b/core/to-observable.ts @@ -1,4 +1,4 @@ -import { MinimumArgumentsRequiredError, ParameterTypeError } from "@xan/observable-internal"; +import { MinimumArgumentsRequiredError, ParameterTypeError } from "@observable/internal"; import { isObservable } from "./is-observable.ts"; import { Observable } from "./observable.ts"; @@ -9,7 +9,7 @@ import { Observable } from "./observable.ts"; * that wraps the original {@linkcode value}. * @example * ```ts - * import { toObservable, Observable } from "@xan/observable-core"; + * import { toObservable, Observable } from "@observable/core"; * * const observableInstance = new Observable((observer) => { * // Implementation omitted for brevity. @@ -21,7 +21,7 @@ import { Observable } from "./observable.ts"; * ``` * @example * ```ts - * import { toObservable, Observable } from "@xan/observable-core"; + * import { toObservable, Observable } from "@observable/core"; * * const customObservable: Observable = { * subscribe(observer) { diff --git a/core/to-observer.test.ts b/core/to-observer.test.ts index 6d5d7a4..8012db9 100644 --- a/core/to-observer.test.ts +++ b/core/to-observer.test.ts @@ -1,4 +1,4 @@ -import { Observer } from "@xan/observable-core"; +import { Observer } from "@observable/core"; import { assertEquals, assertInstanceOf, assertStrictEquals } from "@std/assert"; import { toObserver } from "./to-observer.ts"; diff --git a/core/to-observer.ts b/core/to-observer.ts index 2b404b7..e279721 100644 --- a/core/to-observer.ts +++ b/core/to-observer.ts @@ -1,4 +1,4 @@ -import { MinimumArgumentsRequiredError, ParameterTypeError } from "@xan/observable-internal"; +import { MinimumArgumentsRequiredError, ParameterTypeError } from "@observable/internal"; import { Observer } from "./observer.ts"; import { isObserver } from "./is-observer.ts"; @@ -9,7 +9,7 @@ import { isObserver } from "./is-observer.ts"; * that wraps the original {@linkcode value}. * @example * ```ts - * import { toObserver, Observer } from "@xan/observable-core"; + * import { toObserver, Observer } from "@observable/core"; * * const instance = new Observer((value) => { * // Implementation omitted for brevity. @@ -21,7 +21,7 @@ import { isObserver } from "./is-observer.ts"; * ``` * @example * ```ts - * import { toObserver, Observer } from "@xan/observable-core"; + * import { toObserver, Observer } from "@observable/core"; * * const custom: Observer = { * signal: new AbortController().signal, diff --git a/defer/README.md b/defer/README.md new file mode 100644 index 0000000..7c5a520 --- /dev/null +++ b/defer/README.md @@ -0,0 +1,49 @@ +# @observable/defer + +Creates an [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) that, on +[`subscribe`](https://jsr.io/@observable/core/doc/~/Observable.subscribe), calls an +[`Observable`](https://jsr.io/@observable/core/doc/~/Observable) factory to get an +[`Observable`](https://jsr.io/@observable/core/doc/~/Observable) for each +[`Observer`](https://jsr.io/@observable/core/doc/~/Observer). + +## Example + +```ts +import { defer } from "@observable/defer"; +import { of } from "@observable/of"; + +const controller = new AbortController(); +let values = [1, 2, 3]; +const observable = defer(() => of(values)); + +observable.subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), + return: () => console.log("return"), + throw: (value) => console.error("throw", value), +}); + +// console output: +// "next" 1 +// "next" 2 +// "next" 3 +// "return" + +values = [4, 5, 6]; +observable.subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), + return: () => console.log("return"), + throw: (value) => console.error("throw", value), +}); + +// console output: +// "next" 4 +// "next" 5 +// "next" 6 +// "return" +``` + +# Glossary And Semantics + +[@observable/core](https://jsr.io/@observable/core#glossary-and-semantics) diff --git a/defer/deno.json b/defer/deno.json new file mode 100644 index 0000000..956b11b --- /dev/null +++ b/defer/deno.json @@ -0,0 +1,6 @@ +{ + "name": "@observable/defer", + "version": "0.1.0", + "license": "MIT", + "exports": "./mod.ts" +} diff --git a/defer/mod.test.ts b/defer/mod.test.ts new file mode 100644 index 0000000..8213068 --- /dev/null +++ b/defer/mod.test.ts @@ -0,0 +1,59 @@ +import { assertEquals, assertStrictEquals } from "@std/assert"; +import { Observer } from "@observable/core"; +import { materialize } from "@observable/materialize"; +import type { ObserverNotification } from "@observable/materialize"; +import { pipe } from "@observable/pipe"; +import { of } from "@observable/of"; +import { defer } from "./mod.ts"; + +Deno.test( + "defer should create an Observable that calls a factory to make an Observable for each new Observer", + () => { + // Arrange + let factoryCallCount = 0; + const notifications: Array<[1 | 2, ObserverNotification]> = []; + const source = defer(() => { + factoryCallCount++; + return of([1, 2, 3]); + }); + + // Act + pipe(source, materialize()).subscribe( + new Observer((notification) => notifications.push([1, notification])), + ); + pipe(source, materialize()).subscribe( + new Observer((notification) => notifications.push([2, notification])), + ); + + // Assert + assertStrictEquals(factoryCallCount, 2); + assertEquals(notifications, [ + [1, ["next", 1]], + [1, ["next", 2]], + [1, ["next", 3]], + [1, ["return"]], + [2, ["next", 1]], + [2, ["next", 2]], + [2, ["next", 3]], + [2, ["return"]], + ]); + }, +); + +Deno.test("defer should throw an error if the factory throws an error", () => { + // Arrange + const error = new Error(Math.random().toString()); + const notifications: Array = []; + const source = defer(() => { + throw error; + }); + const materialized = pipe(source, materialize()); + + // Act + materialized.subscribe( + new Observer((notification) => notifications.push(notification)), + ); + + // Assert + assertEquals(notifications, [["throw", error]]); +}); diff --git a/defer/mod.ts b/defer/mod.ts new file mode 100644 index 0000000..f3bcbec --- /dev/null +++ b/defer/mod.ts @@ -0,0 +1,54 @@ +import { Observable, toObservable } from "@observable/core"; +import { MinimumArgumentsRequiredError, ParameterTypeError } from "@observable/internal"; + +/** + * Creates an [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) that, on + * [`subscribe`](https://jsr.io/@observable/core/doc/~/Observable.subscribe), calls an + * [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) {@linkcode factory} to + * get an [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) for each + * [`Observer`](https://jsr.io/@observable/core/doc/~/Observer). + * @example + * ```ts + * import { defer } from "@observable/defer"; + * import { of } from "@observable/of"; + * + * const controller = new AbortController(); + * let values = [1, 2, 3]; + * const observable = defer(() => of(values)); + * + * observable.subscribe({ + * signal: controller.signal, + * next: (value) => console.log("next", value), + * return: () => console.log("return"), + * throw: (value) => console.error("throw", value), + * }); + * + * // console output: + * // "next" 1 + * // "next" 2 + * // "next" 3 + * // "return" + * + * values = [4, 5, 6]; + * observable.subscribe({ + * signal: controller.signal, + * next: (value) => console.log("next", value), + * return: () => console.log("return"), + * throw: (value) => console.error("throw", value), + * }); + * + * // console output: + * // "next" 4 + * // "next" 5 + * // "next" 6 + * // "return" + */ +export function defer( + factory: () => Observable, +): Observable { + if (arguments.length === 0) throw new MinimumArgumentsRequiredError(); + if (typeof factory !== "function") { + throw new ParameterTypeError(0, "Function"); + } + return new Observable((observer) => toObservable(factory()).subscribe(observer)); +} diff --git a/deno.json b/deno.json index b45506e..1f736e8 100644 --- a/deno.json +++ b/deno.json @@ -1,5 +1,36 @@ { - "workspace": ["core", "common", "internal", "web"], + "workspace": [ + "all", + "async-subject", + "behavior-subject", + "broadcast-subject", + "core", + "defer", + "drop", + "empty", + "exhaust-map", + "filter", + "finalize", + "flat", + "flat-map", + "ignore-elements", + "internal", + "map", + "materialize", + "merge", + "merge-map", + "never", + "of", + "pipe", + "race", + "replay-subject", + "switch-map", + "take", + "take-until", + "tap", + "throw-error", + "timer" + ], "license": "MIT", "tasks": { "test": "deno test --watch", diff --git a/drop/README.md b/drop/README.md new file mode 100644 index 0000000..01645b5 --- /dev/null +++ b/drop/README.md @@ -0,0 +1,30 @@ +# @observable/drop + +Drops the first `count` values [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed by +the [source](https://jsr.io/@observable/core#source). + +## Example + +```ts +import { drop } from "@observable/drop"; +import { of } from "@observable/of"; +import { pipe } from "@observable/pipe"; + +const controller = new AbortController(); +pipe(of([1, 2, 3, 4, 5]), drop(2)).subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), + return: () => console.log("return"), + throw: (value) => console.log("throw", value), +}); + +// Console output: +// "next" 3 +// "next" 4 +// "next" 5 +// "return" +``` + +# Glossary And Semantics + +[@observable/core](https://jsr.io/@observable/core#glossary-and-semantics) diff --git a/drop/deno.json b/drop/deno.json new file mode 100644 index 0000000..f8d0f1c --- /dev/null +++ b/drop/deno.json @@ -0,0 +1,6 @@ +{ + "name": "@observable/drop", + "version": "0.1.0", + "license": "MIT", + "exports": "./mod.ts" +} diff --git a/common/drop.test.ts b/drop/mod.test.ts similarity index 83% rename from common/drop.test.ts rename to drop/mod.test.ts index 854cd58..4d2cfe8 100644 --- a/common/drop.test.ts +++ b/drop/mod.test.ts @@ -1,11 +1,10 @@ import { assertEquals, assertStrictEquals } from "@std/assert"; -import { Observer } from "@xan/observable-core"; -import { empty } from "./empty.ts"; -import { of } from "./of.ts"; -import { pipe } from "./pipe.ts"; -import { materialize } from "./materialize.ts"; -import type { ObserverNotification } from "./observer-notification.ts"; -import { drop } from "./drop.ts"; +import { Observer } from "@observable/core"; +import { empty } from "@observable/empty"; +import { of } from "@observable/of"; +import { pipe } from "@observable/pipe"; +import { materialize, type ObserverNotification } from "@observable/materialize"; +import { drop } from "./mod.ts"; Deno.test( "drop should return an empty observable if the count is less than 0", diff --git a/common/drop.ts b/drop/mod.ts similarity index 55% rename from common/drop.ts rename to drop/mod.ts index e6974f8..2e87dae 100644 --- a/common/drop.ts +++ b/drop/mod.ts @@ -1,30 +1,31 @@ -import { isObservable, type Observable } from "@xan/observable-core"; -import { MinimumArgumentsRequiredError, ParameterTypeError } from "@xan/observable-internal"; -import { empty } from "./empty.ts"; -import { pipe } from "./pipe.ts"; -import { filter } from "./filter.ts"; -import { asObservable } from "./as-observable.ts"; +import { isObservable, type Observable, toObservable } from "@observable/core"; +import { MinimumArgumentsRequiredError, ParameterTypeError } from "@observable/internal"; +import { empty } from "@observable/empty"; +import { pipe } from "@observable/pipe"; +import { filter } from "@observable/filter"; /** - * Drops the first {@linkcode count} values [`next`](https://jsr.io/@xan/observable-core/doc/~/Observer.next)ed - * by the [source](https://jsr.io/@xan/observable-core#source). + * Drops the first {@linkcode count} values [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed + * by the [source](https://jsr.io/@observable/core#source). * @example * ```ts - * import { drop, of, pipe } from "@xan/observable-common"; + * import { drop } from "@observable/drop"; + * import { of } from "@observable/of"; + * import { pipe } from "@observable/pipe"; * * const controller = new AbortController(); * pipe(of([1, 2, 3, 4, 5]), drop(2)).subscribe({ * signal: controller.signal, - * next: (value) => console.log(value), + * next: (value) => console.log("next", value), * return: () => console.log("return"), - * throw: (value) => console.log(value), + * throw: (value) => console.log("throw", value), * }); * - * // console output: - * // 3 - * // 4 - * // 5 - * // return + * // Console output: + * // "next" 3 + * // "next" 4 + * // "next" 5 + * // "return" * ``` */ export function drop( @@ -38,7 +39,7 @@ export function drop( if (count < 0 || Number.isNaN(count) || count === Infinity) return empty; return pipe( source, - count === 0 ? asObservable() : filter((_, index) => index >= count), + count === 0 ? toObservable : filter((_, index) => index >= count), ); }; } diff --git a/empty/README.md b/empty/README.md new file mode 100644 index 0000000..a6baab7 --- /dev/null +++ b/empty/README.md @@ -0,0 +1,27 @@ +# @observable/empty + +An [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) that calls +[`return`](https://jsr.io/@observable/core/doc/~/Observer.return) immediately on +[`subscribe`](https://jsr.io/@observable/core/doc/~/Observable.subscribe). + +## Example + +```ts +import { empty } from "@observable/empty"; + +const controller = new AbortController(); + +empty.subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), + return: () => console.log("return"), + throw: (value) => console.log("throw", value), +}); + +// Console output: +// "return" +``` + +# Glossary And Semantics + +[@observable/core](https://jsr.io/@observable/core#glossary-and-semantics) diff --git a/empty/deno.json b/empty/deno.json new file mode 100644 index 0000000..e6dc0e2 --- /dev/null +++ b/empty/deno.json @@ -0,0 +1,6 @@ +{ + "name": "@observable/empty", + "version": "0.1.0", + "license": "MIT", + "exports": "./mod.ts" +} diff --git a/common/empty.test.ts b/empty/mod.test.ts similarity index 84% rename from common/empty.test.ts rename to empty/mod.test.ts index 31220a6..f273b04 100644 --- a/common/empty.test.ts +++ b/empty/mod.test.ts @@ -1,9 +1,8 @@ import { assertEquals } from "@std/assert"; -import { Observer } from "@xan/observable-core"; -import { materialize } from "./materialize.ts"; -import type { ObserverNotification } from "./observer-notification.ts"; -import { empty } from "./empty.ts"; -import { pipe } from "./pipe.ts"; +import { Observer } from "@observable/core"; +import { materialize, type ObserverNotification } from "@observable/materialize"; +import { pipe } from "@observable/pipe"; +import { empty } from "./mod.ts"; Deno.test( "empty should return immediately when subscribed to without a signal", diff --git a/empty/mod.ts b/empty/mod.ts new file mode 100644 index 0000000..ad90c97 --- /dev/null +++ b/empty/mod.ts @@ -0,0 +1,24 @@ +import type { Observable } from "@observable/core"; +import { of } from "@observable/of"; + +/** + * An [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) that calls [`return`](https://jsr.io/@observable/core/doc/~/Observer.return) + * immediately on [`subscribe`](https://jsr.io/@observable/core/doc/~/Observable.subscribe). + * @example + * ```ts + * import { empty } from "@observable/empty"; + * + * const controller = new AbortController(); + * + * empty.subscribe({ + * signal: controller.signal, + * next: (value) => console.log("next", value), + * return: () => console.log("return"), + * throw: (value) => console.log("throw", value), + * }); + * + * // Console output: + * // "return" + * ``` + */ +export const empty: Observable = of([]); diff --git a/exhaust-map/README.md b/exhaust-map/README.md new file mode 100644 index 0000000..579b23f --- /dev/null +++ b/exhaust-map/README.md @@ -0,0 +1,37 @@ +# @observable/exhaust-map + +Projects each [source](https://jsr.io/@observable/core#source) value to an +[`Observable`](https://jsr.io/@observable/core/doc/~/Observable) which is merged in the output +[`Observable`](https://jsr.io/@observable/core/doc/~/Observable) only if the previous projected +[`Observable`](https://jsr.io/@observable/core/doc/~/Observable) has +[`return`](https://jsr.io/@observable/core/doc/~/Observer.return)ed. + +## Example + +```ts +import { exhaustMap } from "@observable/exhaust-map"; +import { of } from "@observable/of"; +import { pipe } from "@observable/pipe"; +import { timer } from "@observable/timer"; + +const controller = new AbortController(); +const source = of([1, 2, 3]); + +pipe( + source, + exhaustMap((value) => pipe(timer(100), map(() => value))), +).subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), + return: () => console.log("return"), + throw: (value) => console.log("throw", value), +}); + +// Console output (after 100ms): +// "next" 1 +// "return" +``` + +# Glossary And Semantics + +[@observable/core](https://jsr.io/@observable/core#glossary-and-semantics) diff --git a/exhaust-map/deno.json b/exhaust-map/deno.json new file mode 100644 index 0000000..f84a888 --- /dev/null +++ b/exhaust-map/deno.json @@ -0,0 +1,6 @@ +{ + "name": "@observable/exhaust-map", + "version": "0.1.0", + "license": "MIT", + "exports": "./mod.ts" +} diff --git a/common/exhaust-map.test.ts b/exhaust-map/mod.test.ts similarity index 96% rename from common/exhaust-map.test.ts rename to exhaust-map/mod.test.ts index 05c9e38..c4af00b 100644 --- a/common/exhaust-map.test.ts +++ b/exhaust-map/mod.test.ts @@ -1,17 +1,16 @@ import { assertEquals } from "@std/assert"; -import { Observable, Observer, Subject } from "@xan/observable-core"; -import { empty } from "./empty.ts"; -import { never } from "./never.ts"; -import { defer } from "./defer.ts"; -import { pipe } from "./pipe.ts"; -import { take } from "./take.ts"; -import { throwError } from "./throw-error.ts"; -import { BehaviorSubject } from "./behavior-subject.ts"; -import { flat } from "./flat.ts"; -import { exhaustMap } from "./exhaust-map.ts"; -import { materialize } from "./materialize.ts"; -import type { ObserverNotification } from "./observer-notification.ts"; -import { map } from "./map.ts"; +import { Observable, Observer, Subject } from "@observable/core"; +import { empty } from "@observable/empty"; +import { never } from "@observable/never"; +import { defer } from "@observable/defer"; +import { pipe } from "@observable/pipe"; +import { take } from "@observable/take"; +import { throwError } from "@observable/throw-error"; +import { BehaviorSubject } from "@observable/behavior-subject"; +import { flat } from "@observable/flat"; +import { exhaustMap } from "./mod.ts"; +import { materialize, type ObserverNotification } from "@observable/materialize"; +import { map } from "@observable/map"; Deno.test( "exhaustMap should map-and-flatten each item to an Observable", diff --git a/common/exhaust-map.ts b/exhaust-map/mod.ts similarity index 61% rename from common/exhaust-map.ts rename to exhaust-map/mod.ts index e5176c2..7b70d35 100644 --- a/common/exhaust-map.ts +++ b/exhaust-map/mod.ts @@ -1,16 +1,17 @@ -import { isObservable, type Observable, Observer } from "@xan/observable-core"; -import { MinimumArgumentsRequiredError, noop, ParameterTypeError } from "@xan/observable-internal"; -import { defer } from "./defer.ts"; -import { pipe } from "./pipe.ts"; -import { tap } from "./tap.ts"; -import { filter } from "./filter.ts"; -import { switchMap } from "./switch-map.ts"; +import { isObservable, type Observable, Observer } from "@observable/core"; +import { MinimumArgumentsRequiredError, noop, ParameterTypeError } from "@observable/internal"; +import { defer } from "@observable/defer"; +import { pipe } from "@observable/pipe"; +import { tap } from "@observable/tap"; +import { filter } from "@observable/filter"; +import { switchMap } from "@observable/switch-map"; /** - * {@linkcode project|Projects} each source value to an [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) - * which is merged in the output [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) only if the previous - * {@linkcode project|projected} [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) has - * [`return`](https://jsr.io/@xan/observable-core/doc/~/Observer.return)ed. + * {@linkcode project|Projects} each [source](https://jsr.io/@observable/core#source) value to an + * [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) which is merged in the output + * [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) only if the previous + * {@linkcode project|projected} [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) has + * [`return`](https://jsr.io/@observable/core/doc/~/Observer.return)ed. */ export function exhaustMap( project: (value: In, index: number) => Observable, diff --git a/filter/README.md b/filter/README.md new file mode 100644 index 0000000..ac449c0 --- /dev/null +++ b/filter/README.md @@ -0,0 +1,29 @@ +# @observable/filter + +Filters [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values from the +[source](https://jsr.io/@observable/core#source) that satisfy a specified predicate. + +## Example + +```ts +import { filter } from "@observable/filter"; +import { of } from "@observable/of"; +import { pipe } from "@observable/pipe"; + +const controller = new AbortController(); +pipe(of([1, 2, 3, 4, 5]), filter((value) => value % 2 === 0)).subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), + return: () => console.log("return"), + throw: (value) => console.log("throw", value), +}); + +// console output: +// "next" 2 +// "next" 4 +// "return" +``` + +# Glossary And Semantics + +[@observable/core](https://jsr.io/@observable/core#glossary-and-semantics) diff --git a/filter/deno.json b/filter/deno.json new file mode 100644 index 0000000..1d9584d --- /dev/null +++ b/filter/deno.json @@ -0,0 +1,6 @@ +{ + "name": "@observable/filter", + "version": "0.1.0", + "license": "MIT", + "exports": "./mod.ts" +} diff --git a/common/filter.test.ts b/filter/mod.test.ts similarity index 87% rename from common/filter.test.ts rename to filter/mod.test.ts index ec026db..29c536a 100644 --- a/common/filter.test.ts +++ b/filter/mod.test.ts @@ -1,10 +1,9 @@ import { assertEquals } from "@std/assert"; -import { Observable, Observer } from "@xan/observable-core"; -import { of } from "./of.ts"; -import { pipe } from "./pipe.ts"; -import { filter } from "./filter.ts"; -import { materialize } from "./materialize.ts"; -import type { ObserverNotification } from "./observer-notification.ts"; +import { Observable, Observer } from "@observable/core"; +import { of } from "@observable/of"; +import { pipe } from "@observable/pipe"; +import { filter } from "./mod.ts"; +import { materialize, type ObserverNotification } from "@observable/materialize"; Deno.test( "filter should filter the items emitted by the source observable", diff --git a/common/filter.ts b/filter/mod.ts similarity index 65% rename from common/filter.ts rename to filter/mod.ts index 453097f..2ba4305 100644 --- a/common/filter.ts +++ b/filter/mod.ts @@ -1,27 +1,27 @@ -import { isObservable, Observable } from "@xan/observable-core"; -import { MinimumArgumentsRequiredError, ParameterTypeError } from "@xan/observable-internal"; -import { pipe } from "./pipe.ts"; -import { asObservable } from "./as-observable.ts"; +import { isObservable, Observable, toObservable } from "@observable/core"; +import { MinimumArgumentsRequiredError, ParameterTypeError } from "@observable/internal"; /** - * Filters [`next`](https://jsr.io/@xan/observable-core/doc/~/Observer.next)ed values from the - * [source](https://jsr.io/@xan/observable-core#source) that satisfy a specified {@linkcode predicate}. + * Filters [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values from the + * [source](https://jsr.io/@observable/core#source) that satisfy a specified {@linkcode predicate}. * @example * ```ts - * import { filter, of, pipe } from "@xan/observable-common"; + * import { filter } from "@observable/filter"; + * import { of } from "@observable/of"; + * import { pipe } from "@observable/pipe"; * * const controller = new AbortController(); * pipe(of([1, 2, 3, 4, 5]), filter((value) => value % 2 === 0)).subscribe({ * signal: controller.signal, - * next: (value) => console.log(value), + * next: (value) => console.log("next", value), * return: () => console.log("return"), - * throw: (value) => console.log(value), + * throw: (value) => console.log("throw", value), * }); * * // console output: - * // 2 - * // 4 - * // return + * // "next" 2 + * // "next" 4 + * // "return" * ``` */ export function filter( @@ -34,7 +34,7 @@ export function filter( return function filterFn(source) { if (arguments.length === 0) throw new MinimumArgumentsRequiredError(); if (!isObservable(source)) throw new ParameterTypeError(0, "Observable"); - source = pipe(source, asObservable()); + source = toObservable(source); return new Observable((observer) => { let index = 0; source.subscribe({ diff --git a/finalize/README.md b/finalize/README.md new file mode 100644 index 0000000..b68161c --- /dev/null +++ b/finalize/README.md @@ -0,0 +1,36 @@ +# @observable/finalize + +The [producer](https://jsr.io/@observable/core#producer) is notifying the +[consumer](https://jsr.io/@observable/core#consumer) that it's done +[`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ing values for any reason, and will +send no more values. Finalization, if it occurs, will always happen as a side-effect _after_ +[`return`](https://jsr.io/@observable/core/doc/~/Observer.return), +[`throw`](https://jsr.io/@observable/core/doc/~/Observer.throw), or +[`unsubscribe`](https://jsr.io/@observable/core/doc/~/Observer.signal) (whichever comes last). + +## Example + +```ts +import { finalize } from "@observable/finalize"; +import { of } from "@observable/of"; +import { pipe } from "@observable/pipe"; + +const controller = new AbortController(); +pipe(of([1, 2, 3]), finalize(() => console.log("finalized"))).subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), + return: () => console.log("return"), + throw: (value) => console.log("throw", value), +}); + +// Console output: +// "next" 1 +// "next" 2 +// "next" 3 +// "return" +// "finalized" +``` + +# Glossary And Semantics + +[@observable/core](https://jsr.io/@observable/core#glossary-and-semantics) diff --git a/finalize/deno.json b/finalize/deno.json new file mode 100644 index 0000000..408d9ac --- /dev/null +++ b/finalize/deno.json @@ -0,0 +1,6 @@ +{ + "name": "@observable/finalize", + "version": "0.1.0", + "license": "MIT", + "exports": "./mod.ts" +} diff --git a/common/finalize.test.ts b/finalize/mod.test.ts similarity index 88% rename from common/finalize.test.ts rename to finalize/mod.test.ts index eb14212..f029785 100644 --- a/common/finalize.test.ts +++ b/finalize/mod.test.ts @@ -1,10 +1,9 @@ import { assertEquals } from "@std/assert"; -import { Observable, Observer } from "@xan/observable-core"; -import { materialize } from "./materialize.ts"; -import type { ObserverNotification } from "./observer-notification.ts"; -import { pipe } from "./pipe.ts"; -import { finalize } from "./finalize.ts"; -import { never } from "./never.ts"; +import { Observable, Observer } from "@observable/core"; +import { materialize, type ObserverNotification } from "@observable/materialize"; +import { pipe } from "@observable/pipe"; +import { finalize } from "./mod.ts"; +import { never } from "@observable/never"; Deno.test( "finalize should call the finalizer function after the source is returned", diff --git a/common/finalize.ts b/finalize/mod.ts similarity index 51% rename from common/finalize.ts rename to finalize/mod.ts index 4393689..d910be5 100644 --- a/common/finalize.ts +++ b/finalize/mod.ts @@ -1,13 +1,32 @@ -import { isObservable, Observable } from "@xan/observable-core"; -import { MinimumArgumentsRequiredError, ParameterTypeError } from "@xan/observable-internal"; -import { pipe } from "./pipe.ts"; -import { asObservable } from "./as-observable.ts"; +import { isObservable, Observable, toObservable } from "@observable/core"; +import { MinimumArgumentsRequiredError, ParameterTypeError } from "@observable/internal"; /** - * The [producer](https://jsr.io/@xan/observable-core#producer) is notifying the [consumer](https://jsr.io/@xan/observable-core#consumer) - * that it's done [`next`](https://jsr.io/@xan/observable-core/doc/~/Observer.next)ing, values for any reason, and will send no more values. Finalization, - * if it occurs, will always happen as a side-effect _after_ [`return`](https://jsr.io/@xan/observable-core/doc/~/Observer.return), - * [`throw`](https://jsr.io/@xan/observable-core/doc/~/Observer.throw), or [`unsubscribe`](https://jsr.io/@xan/observable-core/doc/~/Observer.signal) (whichever comes last). + * The [producer](https://jsr.io/@observable/core#producer) is notifying the [consumer](https://jsr.io/@observable/core#consumer) + * that it's done [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ing, values for any reason, and will send no more values. Finalization, + * if it occurs, will always happen as a side-effect _after_ [`return`](https://jsr.io/@observable/core/doc/~/Observer.return), + * [`throw`](https://jsr.io/@observable/core/doc/~/Observer.throw), or [`unsubscribe`](https://jsr.io/@observable/core/doc/~/Observer.signal) (whichever comes last). + * @example + * ```ts + * import { finalize } from "@observable/finalize"; + * import { of } from "@observable/of"; + * import { pipe } from "@observable/pipe"; + * + * const controller = new AbortController(); + * pipe(of([1, 2, 3]), finalize(() => console.log("finalized"))).subscribe({ + * signal: controller.signal, + * next: (value) => console.log("next", value), + * return: () => console.log("return"), + * throw: (value) => console.log("throw", value), + * }); + * + * // Console output: + * // "next" 1 + * // "next" 2 + * // "next" 3 + * // "return" + * // "finalized" + * ``` */ export function finalize( finalizer: () => void, @@ -19,7 +38,7 @@ export function finalize( return function finalizeFn(source) { if (arguments.length === 0) throw new MinimumArgumentsRequiredError(); if (!isObservable(source)) throw new ParameterTypeError(0, "Observable"); - source = pipe(source, asObservable()); + source = toObservable(source); return new Observable((observer) => { const observerAbortListenerController = new AbortController(); observer.signal.addEventListener("abort", () => finalizer(), { diff --git a/flat-map/README.md b/flat-map/README.md new file mode 100644 index 0000000..9773477 --- /dev/null +++ b/flat-map/README.md @@ -0,0 +1,46 @@ +# @observable/flat-map + +Projects each [source](https://jsr.io/@observable/core#source) value to an +[`Observable`](https://jsr.io/@observable/core/doc/~/Observable) which is merged in the output +[`Observable`](https://jsr.io/@observable/core/doc/~/Observable), in a serialized fashion waiting +for each one to [`return`](https://jsr.io/@observable/core/doc/~/Observer.return) before merging the +next. + +## Example + +```ts +import { flatMap } from "@observable/flat-map"; +import { of } from "@observable/of"; +import { pipe } from "@observable/pipe"; + +const source = of(["a", "b", "c"]); +const controller = new AbortController(); +const observableLookup = { + a: of([1, 2, 3]), + b: of([4, 5, 6]), + c: of([7, 8, 9]), +} as const; + +pipe(source, flatMap((value) => observableLookup[value])).subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), + return: () => console.log("return"), + throw: (value) => console.log("throw", value), +}); + +// Console output: +// "next" 1 +// "next" 2 +// "next" 3 +// "next" 4 +// "next" 5 +// "next" 6 +// "next" 7 +// "next" 8 +// "next" 9 +// "return" +``` + +# Glossary And Semantics + +[@observable/core](https://jsr.io/@observable/core#glossary-and-semantics) diff --git a/flat-map/deno.json b/flat-map/deno.json new file mode 100644 index 0000000..0449de7 --- /dev/null +++ b/flat-map/deno.json @@ -0,0 +1,6 @@ +{ + "name": "@observable/flat-map", + "version": "0.1.0", + "license": "MIT", + "exports": "./mod.ts" +} diff --git a/common/flat-map.test.ts b/flat-map/mod.test.ts similarity index 93% rename from common/flat-map.test.ts rename to flat-map/mod.test.ts index a8951b8..222fa8f 100644 --- a/common/flat-map.test.ts +++ b/flat-map/mod.test.ts @@ -1,10 +1,9 @@ import { assertEquals } from "@std/assert"; -import { type Observable, Observer, Subject } from "@xan/observable-core"; -import { pipe } from "./pipe.ts"; -import { flatMap } from "./flat-map.ts"; -import { map } from "./map.ts"; -import { materialize } from "./materialize.ts"; -import type { ObserverNotification } from "./observer-notification.ts"; +import { type Observable, Observer, Subject } from "@observable/core"; +import { pipe } from "@observable/pipe"; +import { flatMap } from "./mod.ts"; +import { map } from "@observable/map"; +import { materialize, type ObserverNotification } from "@observable/materialize"; Deno.test("flatMap should flatten many inners", () => { // Arrange diff --git a/common/flat-map.ts b/flat-map/mod.ts similarity index 71% rename from common/flat-map.ts rename to flat-map/mod.ts index c095aa9..a45abdf 100644 --- a/common/flat-map.ts +++ b/flat-map/mod.ts @@ -1,17 +1,17 @@ -import { isObservable, Observable } from "@xan/observable-core"; -import { MinimumArgumentsRequiredError, ParameterTypeError } from "@xan/observable-internal"; -import { pipe } from "./pipe.ts"; -import { asObservable } from "./as-observable.ts"; +import { isObservable, Observable, toObservable } from "@observable/core"; +import { MinimumArgumentsRequiredError, ParameterTypeError } from "@observable/internal"; /** - * {@linkcode project|Projects} each [source](https://jsr.io/@xan/observable-core#source) value to an - * [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) which is merged in the output - * [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable), in a serialized fashion - * waiting for each one to [`return`](https://jsr.io/@xan/observable-core/doc/~/Observer.return) before + * {@linkcode project|Projects} each [source](https://jsr.io/@observable/core#source) value to an + * [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) which is merged in the output + * [`Observable`](https://jsr.io/@observable/core/doc/~/Observable), in a serialized fashion + * waiting for each one to [`return`](https://jsr.io/@observable/core/doc/~/Observer.return) before * merging the next. * @example * ```ts - * import { flatMap, pipe, of } from "@xan/observable-common"; + * import { flatMap } from "@observable/flat-map"; + * import { of } from "@observable/of"; + * import { pipe } from "@observable/pipe"; * * const source = of(["a", "b", "c"]); * const controller = new AbortController(); @@ -23,22 +23,22 @@ import { asObservable } from "./as-observable.ts"; * * pipe(source, flatMap((value) => observableLookup[value])).subscribe({ * signal: controller.signal, - * next: (value) => console.log(value), + * next: (value) => console.log("next", value), * return: () => console.log("return"), * throw: (value) => console.log("throw", value), * }); * * // Console output: - * // 1 - * // 2 - * // 3 - * // 4 - * // 5 - * // 6 - * // 7 - * // 8 - * // 9 - * // return + * // "next" 1 + * // "next" 2 + * // "next" 3 + * // "next" 4 + * // "next" 5 + * // "next" 6 + * // "next" 7 + * // "next" 8 + * // "next" 9 + * // "return" * ``` */ export function flatMap( @@ -51,7 +51,7 @@ export function flatMap( return function flatMapFn(source) { if (arguments.length === 0) throw new MinimumArgumentsRequiredError(); if (!isObservable(source)) throw new ParameterTypeError(0, "Observable"); - source = pipe(source, asObservable()); + source = toObservable(source); return new Observable((observer) => { let index = 0; let activeInnerSubscription = false; @@ -81,7 +81,7 @@ export function flatMap( }); function processNextValue(value: In): void { - pipe(project(value, index++), asObservable()).subscribe({ + project(value, index++).subscribe({ signal: observer.signal, next: (value) => observer.next(value), return() { diff --git a/flat/README.md b/flat/README.md new file mode 100644 index 0000000..53cf919 --- /dev/null +++ b/flat/README.md @@ -0,0 +1,38 @@ +# @observable/flat + +Creates an [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) which sequentially emits +all values from the first given [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) and +then moves on to the next. + +## Example + +```ts +import { flat } from "@observable/flat"; +import { of } from "@observable/of"; +import { pipe } from "@observable/pipe"; + +const controller = new AbortController(); + +flat([of([1, 2, 3]), of([4, 5, 6]), of([7, 8, 9])]).subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), + return: () => console.log("return"), + throw: (value) => console.log("throw", value), +}); + +// Console output: +// "next" 1 +// "next" 2 +// "next" 3 +// "next" 4 +// "next" 5 +// "next" 6 +// "next" 7 +// "next" 8 +// "next" 9 +// "return" +``` + +# Glossary And Semantics + +[@observable/core](https://jsr.io/@observable/core#glossary-and-semantics) diff --git a/flat/deno.json b/flat/deno.json new file mode 100644 index 0000000..86ad20b --- /dev/null +++ b/flat/deno.json @@ -0,0 +1,6 @@ +{ + "name": "@observable/flat", + "version": "0.1.0", + "license": "MIT", + "exports": "./mod.ts" +} diff --git a/common/flat.test.ts b/flat/mod.test.ts similarity index 90% rename from common/flat.test.ts rename to flat/mod.test.ts index cce1abc..130061b 100644 --- a/common/flat.test.ts +++ b/flat/mod.test.ts @@ -1,10 +1,9 @@ import { assertEquals } from "@std/assert"; -import { Observer, Subject } from "@xan/observable-core"; -import { pipe } from "./pipe.ts"; -import { throwError } from "./throw-error.ts"; -import { flat } from "./flat.ts"; -import { materialize } from "./materialize.ts"; -import type { ObserverNotification } from "./observer-notification.ts"; +import { Observer, Subject } from "@observable/core"; +import { pipe } from "@observable/pipe"; +import { throwError } from "@observable/throw-error"; +import { flat } from "@observable/flat"; +import { materialize, type ObserverNotification } from "@observable/materialize"; Deno.test("flat should flatten many inners", () => { // Arrange diff --git a/common/flat.ts b/flat/mod.ts similarity index 52% rename from common/flat.ts rename to flat/mod.ts index d4da9bb..f35b292 100644 --- a/common/flat.ts +++ b/flat/mod.ts @@ -1,41 +1,43 @@ -import type { Observable } from "@xan/observable-core"; +import type { Observable } from "@observable/core"; import { identity, isIterable, MinimumArgumentsRequiredError, ParameterTypeError, -} from "@xan/observable-internal"; -import { of } from "./of.ts"; -import { pipe } from "./pipe.ts"; -import { flatMap } from "./flat-map.ts"; +} from "@observable/internal"; +import { of } from "@observable/of"; +import { pipe } from "@observable/pipe"; +import { flatMap } from "@observable/flat-map"; /** - * Creates an [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) which sequentially emits all values from the first given - * [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) and then moves on to the next. + * Creates an [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) which sequentially emits all values from the first given + * [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) and then moves on to the next. * @example * ```ts - * import { flat, pipe, of } from "@xan/observable-common"; + * import { flat } from "@observable/flat"; + * import { of } from "@observable/of"; + * import { pipe } from "@observable/pipe"; * * const controller = new AbortController(); * * flat(of([1, 2, 3]), of([4, 5, 6]), of([7, 8, 9])).subscribe({ * signal: controller.signal, - * next: (value) => console.log(value), + * next: (value) => console.log("next", value), * return: () => console.log("return"), * throw: (value) => console.log("throw", value), * }); * * // Console output: - * // 1 - * // 2 - * // 3 - * // 4 - * // 5 - * // 6 - * // 7 - * // 8 - * // 9 - * // return + * // "next" 1 + * // "next" 2 + * // "next" 3 + * // "next" 4 + * // "next" 5 + * // "next" 6 + * // "next" 7 + * // "next" 8 + * // "next" 9 + * // "return" * ``` */ export function flat( diff --git a/ignore-elements/README.md b/ignore-elements/README.md new file mode 100644 index 0000000..e45589a --- /dev/null +++ b/ignore-elements/README.md @@ -0,0 +1,27 @@ +# @observable/ignore-elements + +Ignores all [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values from the +[source](https://jsr.io/@observable/core#source). + +## Example + +```ts +import { ignoreElements } from "@observable/ignore-elements"; +import { of } from "@observable/of"; +import { pipe } from "@observable/pipe"; + +const controller = new AbortController(); +pipe(of([1, 2, 3, 4, 5]), ignoreElements()).subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), + return: () => console.log("return"), + throw: (value) => console.log("throw", value), +}); + +// console output: +// "return" +``` + +# Glossary And Semantics + +[@observable/core](https://jsr.io/@observable/core#glossary-and-semantics) diff --git a/ignore-elements/deno.json b/ignore-elements/deno.json new file mode 100644 index 0000000..19f9c70 --- /dev/null +++ b/ignore-elements/deno.json @@ -0,0 +1,6 @@ +{ + "name": "@observable/ignore-elements", + "version": "0.1.0", + "license": "MIT", + "exports": "./mod.ts" +} diff --git a/common/ignore-elements.test.ts b/ignore-elements/mod.test.ts similarity index 84% rename from common/ignore-elements.test.ts rename to ignore-elements/mod.test.ts index a55561b..224acdb 100644 --- a/common/ignore-elements.test.ts +++ b/ignore-elements/mod.test.ts @@ -1,10 +1,9 @@ import { assertEquals } from "@std/assert"; -import { Observable, Observer, Subject } from "@xan/observable-core"; -import { pipe } from "./pipe.ts"; -import { of } from "./of.ts"; -import { materialize } from "./materialize.ts"; -import type { ObserverNotification } from "./observer-notification.ts"; -import { ignoreElements } from "./ignore-elements.ts"; +import { Observable, Observer, Subject } from "@observable/core"; +import { pipe } from "@observable/pipe"; +import { of } from "@observable/of"; +import { materialize, type ObserverNotification } from "@observable/materialize"; +import { ignoreElements } from "./mod.ts"; Deno.test( "ignoreElements should ignore all next values but pass return", diff --git a/common/ignore-elements.ts b/ignore-elements/mod.ts similarity index 60% rename from common/ignore-elements.ts rename to ignore-elements/mod.ts index f5e1ea6..aaf7c41 100644 --- a/common/ignore-elements.ts +++ b/ignore-elements/mod.ts @@ -1,25 +1,25 @@ -import { isObservable, Observable } from "@xan/observable-core"; -import { MinimumArgumentsRequiredError, noop, ParameterTypeError } from "@xan/observable-internal"; -import { pipe } from "./pipe.ts"; -import { asObservable } from "./as-observable.ts"; +import { isObservable, Observable, toObservable } from "@observable/core"; +import { MinimumArgumentsRequiredError, noop, ParameterTypeError } from "@observable/internal"; /** - * Ignores all [`next`](https://jsr.io/@xan/observable-core/doc/~/Observer.next)ed values from the - * [source](https://jsr.io/@xan/observable-core#source). + * Ignores all [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values from the + * [source](https://jsr.io/@observable/core#source). * @example * ```ts - * import { ignoreElements, of, pipe } from "@xan/observable-common"; + * import { ignoreElements } from "@observable/ignore-elements"; + * import { of } from "@observable/of"; + * import { pipe } from "@observable/pipe"; * * const controller = new AbortController(); * pipe(of([1, 2, 3, 4, 5]), ignoreElements()).subscribe({ * signal: controller.signal, - * next: (value) => console.log(value), + * next: (value) => console.log("next", value), * return: () => console.log("return"), - * throw: (value) => console.log(value), + * throw: (value) => console.log("throw", value), * }); * * // console output: - * // return + * // "return" * ``` */ export function ignoreElements(): ( @@ -28,7 +28,7 @@ export function ignoreElements(): ( return function ignoreElementsFn(source) { if (arguments.length === 0) throw new MinimumArgumentsRequiredError(); if (!isObservable(source)) throw new ParameterTypeError(0, "Observable"); - source = pipe(source, asObservable()); + source = toObservable(source); return new Observable((observer) => source.subscribe({ signal: observer.signal, diff --git a/internal/README.md b/internal/README.md index 61a6952..364e307 100644 --- a/internal/README.md +++ b/internal/README.md @@ -1,4 +1,4 @@ -# @xan/observable-internal +# @observable/internal -Internal utilities for the `Observable` libraries. Do NOT depend on this library directly as it's an -internal implementation detail. +Internal utilities for the [@observable](https://jsr.io/@observable) libraries. Do NOT depend on +this library directly as it's an internal implementation detail. diff --git a/internal/deno.json b/internal/deno.json index 916d270..16e3e74 100644 --- a/internal/deno.json +++ b/internal/deno.json @@ -1,5 +1,5 @@ { - "name": "@xan/observable-internal", + "name": "@observable/internal", "version": "0.1.0", "license": "MIT", "exports": "./mod.ts" diff --git a/internal/is-abort-signal.ts b/internal/is-abort-signal.ts index b2f79fb..0c2d9d4 100644 --- a/internal/is-abort-signal.ts +++ b/internal/is-abort-signal.ts @@ -5,7 +5,7 @@ import { MinimumArgumentsRequiredError } from "./minimum-arguments-required-erro * Checks if a {@linkcode value} is an object that implements the {@linkcode AbortSignal} interface. * @example * ```ts - * import { isAbortSignal } from "@xan/observable-internal"; + * import { isAbortSignal } from "@observable/internal"; * * const abortSignalInstance = new AbortController().signal; * isAbortSignal(abortSignalInstance); // true diff --git a/internal/is-event-target.ts b/internal/is-event-target.ts index d595460..c0baf8d 100644 --- a/internal/is-event-target.ts +++ b/internal/is-event-target.ts @@ -1,11 +1,11 @@ -import { MinimumArgumentsRequiredError } from "@xan/observable-internal"; +import { MinimumArgumentsRequiredError } from "./minimum-arguments-required-error.ts"; import { isObject } from "./is-object.ts"; /** * Checks if a {@linkcode value} is an object that implements the {@linkcode EventTarget} interface. * @example * ```ts - * import { isEventTarget } from "@xan/observable-internal"; + * import { isEventTarget } from "@observable/internal"; * * const eventTargetInstance = new EventTarget(); * isEventTarget(eventTargetInstance); // true diff --git a/internal/is-iterable.ts b/internal/is-iterable.ts index 53897c6..7e2ac8f 100644 --- a/internal/is-iterable.ts +++ b/internal/is-iterable.ts @@ -5,7 +5,7 @@ import { isObject } from "./is-object.ts"; * Checks if a {@linkcode value} is an object that implements the {@linkcode Iterable} interface. * @example * ```ts - * import { isIterable } from "@xan/observable-internal"; + * import { isIterable } from "@observable/internal"; * * const iterableLiteral: Iterable = { * [Symbol.iterator]() { diff --git a/internal/is-nil.ts b/internal/is-nil.ts index 0cc10ef..fee8e37 100644 --- a/internal/is-nil.ts +++ b/internal/is-nil.ts @@ -1,10 +1,10 @@ -import { MinimumArgumentsRequiredError } from "@xan/observable-internal"; +import { MinimumArgumentsRequiredError } from "./minimum-arguments-required-error.ts"; /** * Checks if a {@linkcode value} is `null` or `undefined`. * @example * ```ts - * import { isNil } from "@xan/observable-internal"; + * import { isNil } from "@observable/internal"; * * isNil(undefined); // true * isNil(null); // true diff --git a/internal/is-object.ts b/internal/is-object.ts index ffb8170..ab7542d 100644 --- a/internal/is-object.ts +++ b/internal/is-object.ts @@ -4,7 +4,7 @@ import { MinimumArgumentsRequiredError } from "./minimum-arguments-required-erro * Checks if a {@linkcode value} is an `object`. * @example * ```ts - * import { isObject } from "@xan/observable-internal"; + * import { isObject } from "@observable/internal"; * * isObject({}); // true * isObject(null)); // false diff --git a/map/README.md b/map/README.md new file mode 100644 index 0000000..5966a7a --- /dev/null +++ b/map/README.md @@ -0,0 +1,30 @@ +# @observable/map + +Projects each value from the [source](https://jsr.io/@observable/core#source) to a new value. + +## Example + +```ts +import { map } from "@observable/map"; +import { of } from "@observable/of"; +import { pipe } from "@observable/pipe"; + +const controller = new AbortController(); + +pipe(of([1, 2, 3]), map((value) => value * 2)).subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), + return: () => console.log("return"), + throw: (value) => console.log("throw", value), +}); + +// Console output: +// "next" 2 +// "next" 4 +// "next" 6 +// "return" +``` + +# Glossary And Semantics + +[@observable/core](https://jsr.io/@observable/core#glossary-and-semantics) diff --git a/map/deno.json b/map/deno.json new file mode 100644 index 0000000..56a9c47 --- /dev/null +++ b/map/deno.json @@ -0,0 +1,6 @@ +{ + "name": "@observable/map", + "version": "0.1.0", + "license": "MIT", + "exports": "./mod.ts" +} diff --git a/common/map.test.ts b/map/mod.test.ts similarity index 88% rename from common/map.test.ts rename to map/mod.test.ts index f2d2b78..48e70ac 100644 --- a/common/map.test.ts +++ b/map/mod.test.ts @@ -1,11 +1,10 @@ import { assertEquals } from "@std/assert"; -import { Observable, Observer } from "@xan/observable-core"; -import { of } from "./of.ts"; -import { pipe } from "./pipe.ts"; -import { throwError } from "./throw-error.ts"; -import { map } from "./map.ts"; -import { materialize } from "./materialize.ts"; -import type { ObserverNotification } from "./observer-notification.ts"; +import { Observable, Observer } from "@observable/core"; +import { of } from "@observable/of"; +import { pipe } from "@observable/pipe"; +import { throwError } from "@observable/throw-error"; +import { map } from "./mod.ts"; +import { materialize, type ObserverNotification } from "@observable/materialize"; Deno.test("map should project the values", () => { // Arrange diff --git a/common/map.ts b/map/mod.ts similarity index 70% rename from common/map.ts rename to map/mod.ts index 1193df0..c8d7a0d 100644 --- a/common/map.ts +++ b/map/mod.ts @@ -1,29 +1,29 @@ -import { isObservable, Observable } from "@xan/observable-core"; -import { MinimumArgumentsRequiredError, ParameterTypeError } from "@xan/observable-internal"; -import { pipe } from "./pipe.ts"; -import { asObservable } from "./as-observable.ts"; +import { isObservable, Observable, toObservable } from "@observable/core"; +import { MinimumArgumentsRequiredError, ParameterTypeError } from "@observable/internal"; /** - * {@linkcode project|Projects} each {@linkcode In|value} from the [source](https://jsr.io/@xan/observable-core#source) + * {@linkcode project|Projects} each {@linkcode In|value} from the [source](https://jsr.io/@observable/core#source) * to a new {@linkcode Out|value}. * @example * ```ts - * import { map, pipe, of } from "@xan/observable-common"; + * import { map } from "@observable/map"; + * import { of } from "@observable/of"; + * import { pipe } from "@observable/pipe"; * * const controller = new AbortController(); * - * pipe(of([1, 2, 3]), map((x) => x * 2)).subscribe({ + * pipe(of([1, 2, 3]), map((value) => value * 2)).subscribe({ * signal: controller.signal, - * next: (value) => console.log(value), + * next: (value) => console.log("next", value), * return: () => console.log("return"), * throw: (value) => console.log("throw", value), * }); * * // Console output: - * // 2 - * // 4 - * // 6 - * // return + * // "next" 2 + * // "next" 4 + * // "next" 6 + * // "return" * ``` */ export function map( @@ -36,7 +36,7 @@ export function map( return function mapFn(source) { if (arguments.length === 0) throw new MinimumArgumentsRequiredError(); if (!isObservable(source)) throw new ParameterTypeError(0, "Observable"); - source = pipe(source, asObservable()); + source = toObservable(source); return new Observable((observer) => { let index = 0; source.subscribe({ diff --git a/materialize/README.md b/materialize/README.md new file mode 100644 index 0000000..b76c109 --- /dev/null +++ b/materialize/README.md @@ -0,0 +1,72 @@ +# @observable/materialize + +Represents all of the notifications from the [source](https://jsr.io/@observable/core#source) as +[`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values marked with their original +types within notification entries. This is especially useful for testing, debugging, and logging. + +## Example + +```ts +import { materialize } from "@observable/materialize"; +import { of } from "@observable/of"; +import { pipe } from "@observable/pipe"; + +const controller = new AbortController(); +pipe(of([1, 2, 3]), materialize()).subscribe({ + signal: controller.signal, + next: (value) => console.log(value), + return: () => console.log("return"), + throw: (value) => console.log("throw", value), +}); + +// Console output: +// ["next", 1] +// ["next", 2] +// ["next", 3] +// ["return"] +// "return" +``` + +## Unit testing example + +```ts +import { materialize, ObserverNotification } from "@observable/materialize"; +import { pipe } from "@observable/pipe"; +import { of } from "@observable/of"; +import { Observer } from "@observable/core"; + +const observable = of([1, 2, 3]); + +describe("observable", () => { + let activeSubscriptionController: AbortController; + + beforeEach(() => (activeSubscriptionController = new AbortController())); + + afterEach(() => activeSubscriptionController?.abort()); + + it("should emit the notifications", () => { + // Arrange + const notifications: Array> = []; + + // Act + pipe(observable, materialize()).subscribe( + new Observer({ + signal: activeSubscriptionController.signal, + next: (notification) => notifications.push(notification), + }), + ); + + // Assert + expect(notifications).toEqual([ + ["next", 1], + ["next", 2], + ["next", 3], + ["return"], + ]); + }); +}); +``` + +# Glossary And Semantics + +[@observable/core](https://jsr.io/@observable/core#glossary-and-semantics) diff --git a/materialize/deno.json b/materialize/deno.json new file mode 100644 index 0000000..2f149b2 --- /dev/null +++ b/materialize/deno.json @@ -0,0 +1,6 @@ +{ + "name": "@observable/materialize", + "version": "0.1.0", + "license": "MIT", + "exports": "./mod.ts" +} diff --git a/common/materialize.test.ts b/materialize/mod.test.ts similarity index 89% rename from common/materialize.test.ts rename to materialize/mod.test.ts index ffaf488..7e84299 100644 --- a/common/materialize.test.ts +++ b/materialize/mod.test.ts @@ -1,10 +1,9 @@ import { assertEquals } from "@std/assert"; -import { Observer } from "@xan/observable-core"; -import { of } from "./of.ts"; -import { pipe } from "./pipe.ts"; -import { throwError } from "./throw-error.ts"; -import { materialize } from "./materialize.ts"; -import type { ObserverNotification } from "./observer-notification.ts"; +import { Observer } from "@observable/core"; +import { of } from "@observable/of"; +import { pipe } from "@observable/pipe"; +import { throwError } from "@observable/throw-error"; +import { materialize, type ObserverNotification } from "./mod.ts"; Deno.test( "materialize should emit the notifications from a source observable that returns", diff --git a/common/materialize.ts b/materialize/mod.ts similarity index 74% rename from common/materialize.ts rename to materialize/mod.ts index 4bd0f7b..709e5ca 100644 --- a/common/materialize.ts +++ b/materialize/mod.ts @@ -1,18 +1,29 @@ -import { isObservable, Observable } from "@xan/observable-core"; -import { MinimumArgumentsRequiredError, ParameterTypeError } from "@xan/observable-internal"; -import type { ObserverNotification } from "./observer-notification.ts"; -import { pipe } from "./pipe.ts"; -import { asObservable } from "./as-observable.ts"; +import { isObservable, Observable, type Observer, toObservable } from "@observable/core"; +import { MinimumArgumentsRequiredError, ParameterTypeError } from "@observable/internal"; + +/** + * Represents any type of [`Observer`](https://jsr.io/@observable/core/doc/~/Observer) notification + * ([`next`](https://jsr.io/@observable/core/doc/~/Observer.next), + * [`return`](https://jsr.io/@observable/core/doc/~/Observer.return), or + * [`throw`](https://jsr.io/@observable/core/doc/~/Observer.throw)). + */ +export type ObserverNotification = Readonly< + | [type: Extract<"next", keyof Observer>, value: Value] + | [type: Extract<"return", keyof Observer>] + | [type: Extract<"throw", keyof Observer>, value: unknown] +>; /** * Represents all of the {@linkcode ObserverNotification|notifications} from the - * [source](https://jsr.io/@xan/observable-core#source) as - * [`next`](https://jsr.io/@xan/observable-core/doc/~/Observer.next)ed values + * [source](https://jsr.io/@observable/core#source) as + * [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values * marked with their original types within {@linkcode ObserverNotification|notification} entries. * This is especially useful for testing, debugging, and logging. * @example * ```ts - * import { materialize, of, pipe } from "@xan/observable-common"; + * import { materialize } from "@observable/materialize"; + * import { of } from "@observable/of"; + * import { pipe } from "@observable/pipe"; * * const controller = new AbortController(); * pipe(of([1, 2, 3]), materialize()).subscribe({ @@ -88,7 +99,7 @@ export function materialize(): ( return function materializeFn(source) { if (arguments.length === 0) throw new MinimumArgumentsRequiredError(); if (!isObservable(source)) throw new ParameterTypeError(0, "Observable"); - source = pipe(source, asObservable()); + source = toObservable(source); return new Observable((observer) => source.subscribe({ signal: observer.signal, diff --git a/merge-map/README.md b/merge-map/README.md new file mode 100644 index 0000000..7a97d67 --- /dev/null +++ b/merge-map/README.md @@ -0,0 +1,45 @@ +# @observable/merge-map + +Projects each [source](https://jsr.io/@observable/core#source) value to an +[`Observable`](https://jsr.io/@observable/core/doc/~/Observable) which is merged in the output +[`Observable`](https://jsr.io/@observable/core/doc/~/Observable). + +## Example + +```ts +import { mergeMap } from "@observable/merge-map"; +import { of } from "@observable/of"; +import { pipe } from "@observable/pipe"; + +const controller = new AbortController(); +const observableLookup = { + 1: of([1, 2, 3]), + 2: of([4, 5, 6]), + 3: of([7, 8, 9]), +} as const; +pipe( + of([1, 2, 3]), + mergeMap((value) => observableLookup[value]), +).subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), + return: () => console.log("return"), + throw: (value) => console.log("throw", value), +}); + +// Console output: +// "next" 1 +// "next" 2 +// "next" 3 +// "next" 4 +// "next" 5 +// "next" 6 +// "next" 7 +// "next" 8 +// "next" 9 +// "return" +``` + +# Glossary And Semantics + +[@observable/core](https://jsr.io/@observable/core#glossary-and-semantics) diff --git a/merge-map/deno.json b/merge-map/deno.json new file mode 100644 index 0000000..e3203c7 --- /dev/null +++ b/merge-map/deno.json @@ -0,0 +1,6 @@ +{ + "name": "@observable/merge-map", + "version": "0.1.0", + "license": "MIT", + "exports": "./mod.ts" +} diff --git a/common/merge-map.test.ts b/merge-map/mod.test.ts similarity index 95% rename from common/merge-map.test.ts rename to merge-map/mod.test.ts index 65ded9a..76e5e3f 100644 --- a/common/merge-map.test.ts +++ b/merge-map/mod.test.ts @@ -1,11 +1,10 @@ import { assertEquals } from "@std/assert"; -import { Observable, Observer, Subject } from "@xan/observable-core"; -import { noop } from "@xan/observable-internal"; -import { pipe } from "./pipe.ts"; -import { map } from "./map.ts"; -import { materialize } from "./materialize.ts"; -import type { ObserverNotification } from "./observer-notification.ts"; -import { mergeMap } from "./merge-map.ts"; +import { Observable, Observer, Subject } from "@observable/core"; +import { noop } from "@observable/internal"; +import { pipe } from "@observable/pipe"; +import { map } from "@observable/map"; +import { materialize, type ObserverNotification } from "@observable/materialize"; +import { mergeMap } from "./mod.ts"; Deno.test("mergeMap should map-and-flatten each item to an Observable", () => { // Arrange diff --git a/common/merge-map.ts b/merge-map/mod.ts similarity index 53% rename from common/merge-map.ts rename to merge-map/mod.ts index 9e54236..9df9826 100644 --- a/common/merge-map.ts +++ b/merge-map/mod.ts @@ -1,11 +1,41 @@ -import { isObservable, Observable } from "@xan/observable-core"; -import { MinimumArgumentsRequiredError, ParameterTypeError } from "@xan/observable-internal"; -import { pipe } from "./pipe.ts"; -import { asObservable } from "./as-observable.ts"; +import { isObservable, Observable, toObservable } from "@observable/core"; +import { MinimumArgumentsRequiredError, ParameterTypeError } from "@observable/internal"; /** - * {@linkcode project|Projects} each source value to an [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) - * which is merged in the output [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable). + * {@linkcode project|Projects} each [source](https://jsr.io/@observable/core#source) value to an + * [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) which is merged in the output + * [`Observable`](https://jsr.io/@observable/core/doc/~/Observable). + * @example + * ```ts + * import { mergeMap } from "@observable/merge-map"; + * import { of } from "@observable/of"; + * import { pipe } from "@observable/pipe"; + * + * const controller = new AbortController(); + * const observableLookup = { + * 1: of([1, 2, 3]), + * 2: of([4, 5, 6]), + * 3: of([7, 8, 9]), + * } as const; + * pipe(of([1, 2, 3]), mergeMap((value) => observableLookup[value])).subscribe({ + * signal: controller.signal, + * next: (value) => console.log("next", value), + * return: () => console.log("return"), + * throw: (value) => console.log("throw", value), + * }); + * + * // Console output: + * // "next" 1 + * // "next" 2 + * // "next" 3 + * // "next" 4 + * // "next" 5 + * // "next" 6 + * // "next" 7 + * // "next" 8 + * // "next" 9 + * // "return" + * ``` */ export function mergeMap( project: (value: In, index: number) => Observable, @@ -18,7 +48,7 @@ export function mergeMap( return function mergeMapFn(source) { if (arguments.length === 0) throw new MinimumArgumentsRequiredError(); if (!isObservable(source)) throw new ParameterTypeError(0, "Observable"); - source = pipe(source, asObservable()); + source = toObservable(source); return new Observable((observer) => { let index = 0; let outerSubscriptionHasReturned = false; @@ -28,7 +58,7 @@ export function mergeMap( signal: observer.signal, next(value) { activeInnerSubscriptions++; - pipe(project(value, index++), asObservable()).subscribe({ + project(value, index++).subscribe({ signal: observer.signal, next: (value) => observer.next(value), return() { diff --git a/merge/README.md b/merge/README.md new file mode 100644 index 0000000..ed5be28 --- /dev/null +++ b/merge/README.md @@ -0,0 +1,39 @@ +# @observable/merge + +Creates and returns an [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) which +concurrently [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)s all values from every +given [source](https://jsr.io/@observable/core#source). + +## Example + +```ts +import { merge } from "@observable/merge"; +import { Subject } from "@observable/core"; +import { of } from "@observable/of"; +import { pipe } from "@observable/pipe"; + +const controller = new AbortController(); +const source1 = new Subject(); +const source2 = new Subject(); +const source3 = new Subject(); + +merge([source1, source2, source3]).subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), + return: () => console.log("return"), + throw: (value) => console.log("throw", value), +}); + +source1.next(1); // "next" 1 +source2.next(2); // "next" 2 +source3.next(3); // "next" 3 +source1.return(); +source1.next(4); // "next" 4 +source2.return(); +source2.next(5); // "next" 5 +source3.return(); // "return" +``` + +# Glossary And Semantics + +[@observable/core](https://jsr.io/@observable/core#glossary-and-semantics) diff --git a/merge/deno.json b/merge/deno.json new file mode 100644 index 0000000..ae5fa2a --- /dev/null +++ b/merge/deno.json @@ -0,0 +1,6 @@ +{ + "name": "@observable/merge", + "version": "0.1.0", + "license": "MIT", + "exports": "./mod.ts" +} diff --git a/common/merge.test.ts b/merge/mod.test.ts similarity index 68% rename from common/merge.test.ts rename to merge/mod.test.ts index 9fe4f6f..f14a1f5 100644 --- a/common/merge.test.ts +++ b/merge/mod.test.ts @@ -1,9 +1,8 @@ -import { merge } from "./merge.ts"; -import { of } from "./of.ts"; -import type { ObserverNotification } from "./observer-notification.ts"; -import { Observer } from "@xan/observable-core"; -import { pipe } from "./pipe.ts"; -import { materialize } from "./materialize.ts"; +import { merge } from "./mod.ts"; +import { of } from "@observable/of"; +import { Observer } from "@observable/core"; +import { pipe } from "@observable/pipe"; +import { materialize, type ObserverNotification } from "@observable/materialize"; import { assertEquals } from "@std/assert"; Deno.test("merge should merge the values", () => { diff --git a/merge/mod.ts b/merge/mod.ts new file mode 100644 index 0000000..daece6d --- /dev/null +++ b/merge/mod.ts @@ -0,0 +1,53 @@ +import type { Observable } from "@observable/core"; +import { + identity, + isIterable, + MinimumArgumentsRequiredError, + ParameterTypeError, +} from "@observable/internal"; +import { of } from "@observable/of"; +import { pipe } from "@observable/pipe"; +import { mergeMap } from "@observable/merge-map"; + +/** + * Creates and returns an [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) which concurrently + * [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)s all values from every given + * [source](https://jsr.io/@observable/core#source). + * @example + * ```ts + * import { merge } from "@observable/merge"; + * import { Subject } from "@observable/core"; + * import { of } from "@observable/of"; + * import { pipe } from "@observable/pipe"; + * + * const controller = new AbortController(); + * const source1 = new Subject(); + * const source2 = new Subject(); + * const source3 = new Subject(); + * + * pipe(merge([source1, source2, source3]).subscribe({ + * signal: controller.signal, + * next: (value) => console.log("next", value), + * return: () => console.log("return"), + * throw: (value) => console.log("throw", value), + * }); + * + * source1.next(1); // "next" 1 + * source2.next(2); // "next" 2 + * source3.next(3); // "next" 3 + * source1.return(); + * source1.next(4); // "next" 4 + * source2.return(); + * source2.next(5); // "next" 5 + * source3.return(); // "return" + * ``` + */ +export function merge( + // Accepting an Iterable is a design choice for performance (iterables are lazily evaluated) and + // flexibility (can accept any iterable, not just arrays). + sources: Iterable>, +): Observable { + if (arguments.length === 0) throw new MinimumArgumentsRequiredError(); + if (!isIterable(sources)) throw new ParameterTypeError(0, "Iterable"); + return pipe(of(sources), mergeMap(identity)); +} diff --git a/never/README.md b/never/README.md new file mode 100644 index 0000000..01c29ee --- /dev/null +++ b/never/README.md @@ -0,0 +1,23 @@ +# @observable/never + +An [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) that does nothing on +[`subscribe`](https://jsr.io/@observable/core/doc/~/Observable.subscribe). + +## Example + +```ts +import { never } from "@observable/never"; + +const controller = new AbortController(); + +never.subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), // Never called + return: () => console.log("return"), // Never called + throw: (value) => console.log("throw", value), // Never called +}); +``` + +# Glossary And Semantics + +[@observable/core](https://jsr.io/@observable/core#glossary-and-semantics) diff --git a/never/deno.json b/never/deno.json new file mode 100644 index 0000000..ea1ee53 --- /dev/null +++ b/never/deno.json @@ -0,0 +1,6 @@ +{ + "name": "@observable/never", + "version": "0.1.0", + "license": "MIT", + "exports": "./mod.ts" +} diff --git a/common/never.test.ts b/never/mod.test.ts similarity index 80% rename from common/never.test.ts rename to never/mod.test.ts index dfcf982..41a97f1 100644 --- a/common/never.test.ts +++ b/never/mod.test.ts @@ -1,9 +1,8 @@ import { assertEquals } from "@std/assert"; -import { Observer } from "@xan/observable-core"; -import { materialize } from "./materialize.ts"; -import type { ObserverNotification } from "./observer-notification.ts"; -import { never } from "./never.ts"; -import { pipe } from "./pipe.ts"; +import { Observer } from "@observable/core"; +import { materialize, type ObserverNotification } from "@observable/materialize"; +import { never } from "./mod.ts"; +import { pipe } from "@observable/pipe"; Deno.test( "never should not emit when subscribed to with an aborted signal", diff --git a/never/mod.ts b/never/mod.ts new file mode 100644 index 0000000..d0d5c3f --- /dev/null +++ b/never/mod.ts @@ -0,0 +1,21 @@ +import { Observable } from "@observable/core"; +import { noop } from "@observable/internal"; + +/** + * An [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) that does nothing on + * [`subscribe`](https://jsr.io/@observable/core/doc/~/Observable.subscribe). + * @example + * ```ts + * import { never } from "@observable/never"; + * + * const controller = new AbortController(); + * + * never.subscribe({ + * signal: controller.signal, + * next: (value) => console.log("next", value), // Never called + * return: () => console.log("return"), // Never called + * throw: (value) => console.log("throw", value), // Never called + * }); + * ``` + */ +export const never: Observable = new Observable(noop); diff --git a/of/README.md b/of/README.md new file mode 100644 index 0000000..2d3322f --- /dev/null +++ b/of/README.md @@ -0,0 +1,54 @@ +# @observable/of + +Creates and returns an [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) that emits a +sequence of values in order on +[`subscribe`](https://jsr.io/@observable/core/doc/~/Observable.subscribe) and then +[`return`](https://jsr.io/@observable/core/doc/~/Observer.return)s. + +## Example + +```ts +import { of } from "@observable/of"; + +const controller = new AbortController(); + +of([1, 2, 3]).subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), + return: () => console.log("return"), + throw: (value) => console.error("throw", value), +}); + +// console output: +// "next" 1 +// "next" 2 +// "next" 3 +// "return" +``` + +## Example with early unsubscription + +```ts +import { of } from "@observable/of"; + +let count = 0; +const controller = new AbortController(); + +of([1, 2, 3]).subscribe({ + signal: controller.signal, + next(value) { + console.log("next", value); + if (value === 2) controller.abort(); + }, + return: () => console.log("return"), + throw: (value) => console.error("throw", value), +}); + +// console output: +// "next" 1 +// "next" 2 +``` + +# Glossary And Semantics + +[@observable/core](https://jsr.io/@observable/core#glossary-and-semantics) diff --git a/of/deno.json b/of/deno.json new file mode 100644 index 0000000..90c8263 --- /dev/null +++ b/of/deno.json @@ -0,0 +1,6 @@ +{ + "name": "@observable/of", + "version": "0.1.0", + "license": "MIT", + "exports": "./mod.ts" +} diff --git a/common/of.test.ts b/of/mod.test.ts similarity index 78% rename from common/of.test.ts rename to of/mod.test.ts index d198543..e6ef89f 100644 --- a/common/of.test.ts +++ b/of/mod.test.ts @@ -1,9 +1,8 @@ import { assertEquals } from "@std/assert"; -import { Observer } from "@xan/observable-core"; -import { materialize } from "./materialize.ts"; -import type { ObserverNotification } from "./observer-notification.ts"; -import { pipe } from "./pipe.ts"; -import { of } from "./of.ts"; +import { Observer } from "@observable/core"; +import { of } from "./mod.ts"; +import { pipe } from "@observable/pipe"; +import { materialize, type ObserverNotification } from "@observable/materialize"; Deno.test("of should return empty if no values are provided", () => { // Arrange diff --git a/common/of.ts b/of/mod.ts similarity index 66% rename from common/of.ts rename to of/mod.ts index b2146b4..2a32f40 100644 --- a/common/of.ts +++ b/of/mod.ts @@ -1,35 +1,35 @@ -import { Observable } from "@xan/observable-core"; +import { Observable } from "@observable/core"; import { isIterable, MinimumArgumentsRequiredError, ParameterTypeError, -} from "@xan/observable-internal"; +} from "@observable/internal"; /** - * Creates and returns an [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) that emits a sequence of {@linkcode values} in order on - * [`subscribe`](https://jsr.io/@xan/observable-core/doc/~/Observable.subscribe) and then [`return`](https://jsr.io/@xan/observable-core/doc/~/Observer.return)s. + * Creates and returns an [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) that emits a sequence of {@linkcode values} in order on + * [`subscribe`](https://jsr.io/@observable/core/doc/~/Observable.subscribe) and then [`return`](https://jsr.io/@observable/core/doc/~/Observer.return)s. * @example * ```ts - * import { of } from "@xan/observable-common"; + * import { of } from "@observable/of"; * * const controller = new AbortController(); * * of([1, 2, 3]).subscribe({ * signal: controller.signal, - * next: (value) => console.log(value), + * next: (value) => console.log("next", value), * return: () => console.log("return"), * throw: (value) => console.error("throw", value), * }); * * // console output: - * // 1 - * // 2 - * // 3 - * // return + * // "next" 1 + * // "next" 2 + * // "next" 3 + * // "return" * ``` * @example * ```ts - * import { of } from "@xan/observable-common"; + * import { of } from "@observable/of"; * * let count = 0; * const controller = new AbortController(); @@ -37,16 +37,16 @@ import { * of([1, 2, 3]).subscribe({ * signal: controller.signal, * next(value) { + * console.log("next", value); * if (value === 2) controller.abort(); - * console.log(value); * }, * return: () => console.log("return"), * throw: (value) => console.error("throw", value), * }); * * // console output: - * // 1 - * // 2 + * // "next" 1 + * // "next" 2 * ``` */ export function of( diff --git a/pipe/README.md b/pipe/README.md new file mode 100644 index 0000000..af384ad --- /dev/null +++ b/pipe/README.md @@ -0,0 +1,34 @@ +# @observable/pipe + +Pipe a value through a series of unary functions. + +## Example + +```ts +import { pipe } from "@observable/pipe"; +import { of } from "@observable/of"; +import { map } from "@observable/map"; +import { filter } from "@observable/filter"; + +const controller = new AbortController(); + +pipe( + of([1, 2, 3, 4, 5]), + filter((value) => value % 2 === 0), + map((value) => value * 2), +).subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), + return: () => console.log("return"), + throw: (value) => console.log("throw", value), +}); + +// Console output: +// "next" 4 +// "next" 8 +// "return" +``` + +# Glossary And Semantics + +[@observable/core](https://jsr.io/@observable/core#glossary-and-semantics) diff --git a/pipe/deno.json b/pipe/deno.json new file mode 100644 index 0000000..acfccb5 --- /dev/null +++ b/pipe/deno.json @@ -0,0 +1,6 @@ +{ + "name": "@observable/pipe", + "version": "0.1.0", + "license": "MIT", + "exports": "./mod.ts" +} diff --git a/common/pipe.test.ts b/pipe/mod.test.ts similarity index 87% rename from common/pipe.test.ts rename to pipe/mod.test.ts index dcd61ab..e33a4e5 100644 --- a/common/pipe.test.ts +++ b/pipe/mod.test.ts @@ -1,4 +1,4 @@ -import { pipe } from "./pipe.ts"; +import { pipe } from "./mod.ts"; import { assertStrictEquals } from "@std/assert"; Deno.test("pipe should allow any kind of custom piping", () => { diff --git a/common/pipe.ts b/pipe/mod.ts similarity index 99% rename from common/pipe.ts rename to pipe/mod.ts index e0a3941..ad45f42 100644 --- a/common/pipe.ts +++ b/pipe/mod.ts @@ -1,4 +1,4 @@ -import { MinimumArgumentsRequiredError, ParameterTypeError } from "@xan/observable-internal"; +import { MinimumArgumentsRequiredError, ParameterTypeError } from "@observable/internal"; /** * A unary function that takes a {@linkcode source|value} and returns it. diff --git a/race/README.md b/race/README.md new file mode 100644 index 0000000..e83705f --- /dev/null +++ b/race/README.md @@ -0,0 +1,38 @@ +# @observable/race + +Creates and returns an [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) that mirrors +the first [source](https://jsr.io/@observable/core#source) to +[`next`](https://jsr.io/@observable/core/doc/~/Observer.next) or +[`throw`](https://jsr.io/@observable/core/doc/~/Observer.throw) a value. + +## Example + +```ts +import { race } from "@observable/race"; +import { Subject } from "@observable/core"; + +const controller = new AbortController(); +const source1 = new Subject(); +const source2 = new Subject(); +const source3 = new Subject(); + +race([source1, source2, source3]).subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), + return: () => console.log("return"), + throw: (value) => console.log("throw", value), +}); + +source2.next(1); // "next" 1 +source1.next(2); +source3.next(3); +source1.return(); +source2.next(4); // "next" 4 +source2.return(); +source3.next(5); +source2.return(); // "return" +``` + +# Glossary And Semantics + +[@observable/core](https://jsr.io/@observable/core#glossary-and-semantics) diff --git a/race/deno.json b/race/deno.json new file mode 100644 index 0000000..69e52d2 --- /dev/null +++ b/race/deno.json @@ -0,0 +1,6 @@ +{ + "name": "@observable/race", + "version": "0.1.0", + "license": "MIT", + "exports": "./mod.ts" +} diff --git a/common/race.test.ts b/race/mod.test.ts similarity index 84% rename from common/race.test.ts rename to race/mod.test.ts index b32e7dc..12f1bc3 100644 --- a/common/race.test.ts +++ b/race/mod.test.ts @@ -1,15 +1,14 @@ import { assertEquals } from "@std/assert"; -import { Observer, Subject } from "@xan/observable-core"; -import { empty } from "./empty.ts"; -import { never } from "./never.ts"; -import { pipe } from "./pipe.ts"; -import { throwError } from "./throw-error.ts"; -import { materialize } from "./materialize.ts"; -import type { ObserverNotification } from "./observer-notification.ts"; -import { race } from "./race.ts"; -import { defer } from "./defer.ts"; -import { of } from "./of.ts"; -import { flat } from "./flat.ts"; +import { Observer, Subject } from "@observable/core"; +import { empty } from "@observable/empty"; +import { never } from "@observable/never"; +import { pipe } from "@observable/pipe"; +import { throwError } from "@observable/throw-error"; +import { materialize, type ObserverNotification } from "@observable/materialize"; +import { race } from "./mod.ts"; +import { defer } from "@observable/defer"; +import { of } from "@observable/of"; +import { flat } from "@observable/flat"; Deno.test( "race should mirror the first source observable to emit an item and ignore the others", diff --git a/race/mod.ts b/race/mod.ts new file mode 100644 index 0000000..8ef53e7 --- /dev/null +++ b/race/mod.ts @@ -0,0 +1,76 @@ +import { type Observable, Observer } from "@observable/core"; +import { + isIterable, + MinimumArgumentsRequiredError, + noop, + ParameterTypeError, +} from "@observable/internal"; +import { defer } from "@observable/defer"; +import { of } from "@observable/of"; +import { pipe } from "@observable/pipe"; +import { tap } from "@observable/tap"; +import { mergeMap } from "@observable/merge-map"; +import { takeUntil } from "@observable/take-until"; +import { filter } from "@observable/filter"; +import { AsyncSubject } from "@observable/async-subject"; + +/** + * Creates and returns an [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) that mirrors the first + * [source](https://jsr.io/@observable/core#source) to [`next`](https://jsr.io/@observable/core/doc/~/Observer.next) or + * [`throw`](https://jsr.io/@observable/core/doc/~/Observer.throw) a value. + * @example + * ```ts + * import { race } from "@observable/race"; + * import { Subject } from "@observable/core"; + * + * const controller = new AbortController(); + * const source1 = new Subject(); + * const source2 = new Subject(); + * const source3 = new Subject(); + * + * race([source1, source2, source3].subscribe({ + * signal: controller.signal, + * next: (value) => console.log("next", value), + * return: () => console.log("return"), + * throw: (value) => console.log("throw", value), + * }); + * + * source2.next(1); // "next" 1 + * source1.next(2); + * source3.next(3); + * source1.return(); + * source2.next(4); // "next" 4 + * source2.return(); + * source3.next(5); + * source2.return(); // "return" + * ``` + */ +export function race( + // Accepting an Iterable is a design choice for performance (iterables are lazily evaluated) and + // flexibility (can accept any iterable, not just arrays). + sources: Iterable>, +): Observable { + if (arguments.length === 0) throw new MinimumArgumentsRequiredError(); + if (!isIterable(sources)) throw new ParameterTypeError(0, "Iterable"); + return defer(() => { + const finished = new AsyncSubject(); + return pipe( + of(sources), + takeUntil(finished), + mergeMap((source, index) => { + const observer = new Observer({ next: finish, throw: noop }); + const lost = pipe(finished, filter(isLoser)); + return pipe(source, tap(observer), takeUntil(lost)); + + function finish(): void { + finished.next(index); + finished.return(); + } + + function isLoser(winnerIndex: number): boolean { + return winnerIndex !== index; + } + }), + ); + }); +} diff --git a/replay-subject/README.md b/replay-subject/README.md new file mode 100644 index 0000000..a94c90d --- /dev/null +++ b/replay-subject/README.md @@ -0,0 +1,54 @@ +# @observable/replay-subject + +A variant of [`Subject`](https://jsr.io/@observable/core/doc/~/Subject) that replays buffered +[`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values upon +[`subscription`](https://jsr.io/@observable/core/doc/~/Observable.subscribe). + +## Example + +```ts +import { ReplaySubject } from "@observable/replay-subject"; + +const subject = new ReplaySubject(3); +const controller = new AbortController(); + +subject.next(1); // Stored in buffer +subject.next(2); // Stored in buffer +subject.next(3); // Stored in buffer +subject.next(4); // Stored in buffer and 1 gets trimmed off + +subject.subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), + return: () => console.log("return"), + throw: (value) => console.log("throw", value), +}); + +// Console output: +// "next" 2 +// "next" 3 +// "next" 4 + +// Values pushed after the subscribe will emit immediately +// unless the subject is already finalized. +subject.next(5); // Stored in buffer and 2 gets trimmed off + +// Console output: +// "next" 5 + +subject.subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), + return: () => console.log("return"), + throw: (value) => console.log("throw", value), +}); + +// Console output: +// "next" 3 +// "next" 4 +// "next" 5 +``` + +# Glossary And Semantics + +[@observable/core](https://jsr.io/@observable/core#glossary-and-semantics) diff --git a/replay-subject/deno.json b/replay-subject/deno.json new file mode 100644 index 0000000..423f66f --- /dev/null +++ b/replay-subject/deno.json @@ -0,0 +1,6 @@ +{ + "name": "@observable/replay-subject", + "version": "0.1.0", + "license": "MIT", + "exports": "./mod.ts" +} diff --git a/common/replay-subject.test.ts b/replay-subject/mod.test.ts similarity index 95% rename from common/replay-subject.test.ts rename to replay-subject/mod.test.ts index 6b516b7..59ddad4 100644 --- a/common/replay-subject.test.ts +++ b/replay-subject/mod.test.ts @@ -1,11 +1,10 @@ import { assertEquals, assertStrictEquals, assertThrows } from "@std/assert"; -import { Observer } from "@xan/observable-core"; -import { noop } from "@xan/observable-internal"; -import { of } from "./of.ts"; -import { pipe } from "./pipe.ts"; -import { materialize } from "./materialize.ts"; -import type { ObserverNotification } from "./observer-notification.ts"; -import { ReplaySubject } from "./replay-subject.ts"; +import { Observer } from "@observable/core"; +import { noop } from "@observable/internal"; +import { of } from "@observable/of"; +import { pipe } from "@observable/pipe"; +import { materialize, type ObserverNotification } from "@observable/materialize"; +import { ReplaySubject } from "./mod.ts"; Deno.test("ReplaySubject.toString should be '[object ReplaySubject]'", () => { // Arrange / Act / Assert diff --git a/common/replay-subject.ts b/replay-subject/mod.ts similarity index 54% rename from common/replay-subject.ts rename to replay-subject/mod.ts index e856994..4f6b6fb 100644 --- a/common/replay-subject.ts +++ b/replay-subject/mod.ts @@ -1,19 +1,74 @@ -import { isObserver, type Observable, type Observer, Subject } from "@xan/observable-core"; +import { isObserver, type Observable, type Observer, Subject } from "@observable/core"; import { InstanceofError, MinimumArgumentsRequiredError, ParameterTypeError, -} from "@xan/observable-internal"; -import { flat } from "./flat.ts"; -import { defer } from "./defer.ts"; -import { of } from "./of.ts"; -import type { ReplaySubjectConstructor } from "./replay-subject-constructor.ts"; +} from "@observable/internal"; +import { flat } from "@observable/flat"; +import { defer } from "@observable/defer"; +import { of } from "@observable/of"; /** - * Object type that acts as a variant of [`Subject`](https://jsr.io/@xan/observable-core/doc/~/Subject). + * Object type that acts as a variant of [`Subject`](https://jsr.io/@observable/core/doc/~/Subject). */ export type ReplaySubject = Subject; +/** + * Object interface for an {@linkcode ReplaySubject} factory. + */ +export interface ReplaySubjectConstructor { + /** + * Creates and returns an object that acts as a [`Subject`](https://jsr.io/@observable/core/doc/~/Subject) that replays + * buffered [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values upon + * [`subscription`](https://jsr.io/@observable/core/doc/~/Observable.subscribe). + * @example + * ```ts + * import { ReplaySubject } from "@observable/replay-subject"; + * + * const subject = new ReplaySubject(3); + * const controller = new AbortController(); + * + * subject.next(1); // Stored in buffer + * subject.next(2); // Stored in buffer + * subject.next(3); // Stored in buffer + * subject.next(4); // Stored in buffer and 1 gets trimmed off + * + * subject.subscribe({ + * signal: controller.signal, + * next: (value) => console.log("next", value), + * return: () => console.log("return"), + * throw: (value) => console.log("throw", value), + * }); + * + * // Console output: + * // "next" 2 + * // "next" 3 + * // "next" 4 + * + * // Values pushed after the subscribe will emit immediately + * // unless the subject is already finalized. + * subject.next(5); // Stored in buffer and 2 gets trimmed off + * + * // Console output: + * // "next" 5 + * + * subject.subscribe({ + * signal: controller.signal, + * next: (value) => console.log("next", value), + * return: () => console.log("return"), + * throw: (value) => console.log("throw", value), + * }); + * + * // Console output: + * // "next" 3 + * // "next" 4 + * // "next" 5 + * ``` + */ + new (bufferSize: number): ReplaySubject; + readonly prototype: ReplaySubject; +} + export const ReplaySubject: ReplaySubjectConstructor = class { readonly [Symbol.toStringTag] = "ReplaySubject"; readonly #bufferSize: number; diff --git a/switch-map/README.md b/switch-map/README.md new file mode 100644 index 0000000..a1d22dd --- /dev/null +++ b/switch-map/README.md @@ -0,0 +1,38 @@ +# @observable/switch-map + +Projects each [source](https://jsr.io/@observable/core#source) value to an +[`Observable`](https://jsr.io/@observable/core/doc/~/Observable) which is merged in the output +[`Observable`](https://jsr.io/@observable/core/doc/~/Observable), emitting values only from the most +recently projected [`Observable`](https://jsr.io/@observable/core/doc/~/Observable). + +## Example + +```ts +import { switchMap } from "@observable/switch-map"; +import { of } from "@observable/of"; +import { pipe } from "@observable/pipe"; + +const controller = new AbortController(); +const observableLookup = { + 1: of([1, 2, 3]), + 2: of([4, 5, 6]), + 3: of([7, 8, 9]), +} as const; + +pipe(of([1, 2, 3]), switchMap((value) => observableLookup[value])).subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), + return: () => console.log("return"), + throw: (value) => console.log("throw", value), +}); + +// Console output: +// "next" 7 +// "next" 8 +// "next" 9 +// "return" +``` + +# Glossary And Semantics + +[@observable/core](https://jsr.io/@observable/core#glossary-and-semantics) diff --git a/switch-map/deno.json b/switch-map/deno.json new file mode 100644 index 0000000..3f9fc45 --- /dev/null +++ b/switch-map/deno.json @@ -0,0 +1,6 @@ +{ + "name": "@observable/switch-map", + "version": "0.1.0", + "license": "MIT", + "exports": "./mod.ts" +} diff --git a/common/switch-map.test.ts b/switch-map/mod.test.ts similarity index 96% rename from common/switch-map.test.ts rename to switch-map/mod.test.ts index f58148d..9f215e2 100644 --- a/common/switch-map.test.ts +++ b/switch-map/mod.test.ts @@ -1,17 +1,16 @@ import { assertEquals } from "@std/assert"; -import { Observable, Observer, Subject } from "@xan/observable-core"; -import { empty } from "./empty.ts"; -import { never } from "./never.ts"; -import { defer } from "./defer.ts"; -import { pipe } from "./pipe.ts"; -import { take } from "./take.ts"; -import { throwError } from "./throw-error.ts"; -import { BehaviorSubject } from "./behavior-subject.ts"; -import { flat } from "./flat.ts"; -import { switchMap } from "./switch-map.ts"; -import { materialize } from "./materialize.ts"; -import type { ObserverNotification } from "./observer-notification.ts"; -import { map } from "./map.ts"; +import { Observable, Observer, Subject } from "@observable/core"; +import { empty } from "@observable/empty"; +import { never } from "@observable/never"; +import { defer } from "@observable/defer"; +import { pipe } from "@observable/pipe"; +import { take } from "@observable/take"; +import { throwError } from "@observable/throw-error"; +import { BehaviorSubject } from "@observable/behavior-subject"; +import { flat } from "@observable/flat"; +import { switchMap } from "./mod.ts"; +import { map } from "@observable/map"; +import { materialize, type ObserverNotification } from "@observable/materialize"; Deno.test("switchMap should map-and-flatten each item to an Observable", () => { // Arrange diff --git a/switch-map/mod.ts b/switch-map/mod.ts new file mode 100644 index 0000000..b6e8e3a --- /dev/null +++ b/switch-map/mod.ts @@ -0,0 +1,54 @@ +import { isObservable, type Observable, Observer, Subject, toObservable } from "@observable/core"; +import { MinimumArgumentsRequiredError, noop, ParameterTypeError } from "@observable/internal"; +import { defer } from "@observable/defer"; +import { pipe } from "@observable/pipe"; +import { takeUntil } from "@observable/take-until"; +import { tap } from "@observable/tap"; +import { mergeMap } from "@observable/merge-map"; + +/** + * {@linkcode project|Projects} each [source](https://jsr.io/@observable/core#source) value to an + * [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) which is merged in the output + * [`Observable`](https://jsr.io/@observable/core/doc/~/Observable), emitting values only from the most + * recently {@linkcode project|projected} [`Observable`](https://jsr.io/@observable/core/doc/~/Observable). + * @example + * ```ts + * import { BehaviorSubject } from "@observable/behavior-subject"; + * import { switchMap } from "@observable/switch-map"; + * import { of } from "@observable/of"; + * import { pipe } from "@observable/pipe"; + * + * const page = new BehaviorSubject(1); + * const controller = new AbortController(); + * pipe(page, switchMap((value) => fetchPage(value))).subscribe({ + * signal: controller.signal, + * next: (value) => console.log("next", value), + * return: () => console.log("return"), + * throw: (value) => console.log("throw", value), + * }); + * + * function fetchPage(page: number): Observable { + * return of(`Page ${page}`); + * } + */ +export function switchMap( + project: (value: In, index: number) => Observable, +): (source: Observable) => Observable { + if (arguments.length === 0) throw new MinimumArgumentsRequiredError(); + if (typeof project !== "function") { + throw new ParameterTypeError(0, "Function"); + } + return function switchMapFn(source) { + if (arguments.length === 0) throw new MinimumArgumentsRequiredError(); + if (!isObservable(source)) throw new ParameterTypeError(0, "Observable"); + source = toObservable(source); + return defer(() => { + const switching = new Subject(); + return pipe( + source, + tap(new Observer({ next: () => switching.next(), throw: noop })), + mergeMap((value, index) => pipe(project(value, index), takeUntil(switching))), + ); + }); + }; +} diff --git a/take-until/README.md b/take-until/README.md new file mode 100644 index 0000000..7d24ead --- /dev/null +++ b/take-until/README.md @@ -0,0 +1,35 @@ +# @observable/take-until + +Takes [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values from the +[source](https://jsr.io/@observable/core#source) until +[notified](https://jsr.io/@observable/core#notifier) to stop. + +## Example + +```ts +import { Subject } from "@observable/core"; +import { takeUntil } from "@observable/take-until"; +import { of } from "@observable/of"; +import { pipe } from "@observable/pipe"; + +const controller = new AbortController(); +const source = new Subject(); +const notifier = new Subject(); + +pipe(source, takeUntil(notifier)).subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), + return: () => console.log("return"), + throw: (value) => console.log("throw", value), +}); + +source.next(1); // "next" 1 +source.next(2); // "next" 2 +notifier.next(); // "return" +source.next(3); +source.return(); +``` + +# Glossary And Semantics + +[@observable/core](https://jsr.io/@observable/core#glossary-and-semantics) diff --git a/take-until/deno.json b/take-until/deno.json new file mode 100644 index 0000000..7e4a486 --- /dev/null +++ b/take-until/deno.json @@ -0,0 +1,6 @@ +{ + "name": "@observable/take-until", + "version": "0.1.0", + "license": "MIT", + "exports": "./mod.ts" +} diff --git a/common/take-until.test.ts b/take-until/mod.test.ts similarity index 91% rename from common/take-until.test.ts rename to take-until/mod.test.ts index 41a38d9..335c353 100644 --- a/common/take-until.test.ts +++ b/take-until/mod.test.ts @@ -1,11 +1,10 @@ import { assertEquals } from "@std/assert"; -import { Observable, Observer, Subject } from "@xan/observable-core"; -import { noop } from "@xan/observable-internal"; -import { of } from "./of.ts"; -import { pipe } from "./pipe.ts"; -import { takeUntil } from "./take-until.ts"; -import { materialize } from "./materialize.ts"; -import type { ObserverNotification } from "./observer-notification.ts"; +import { Observable, Observer, Subject } from "@observable/core"; +import { noop } from "@observable/internal"; +import { of } from "@observable/of"; +import { pipe } from "@observable/pipe"; +import { takeUntil } from "./mod.ts"; +import { materialize, type ObserverNotification } from "@observable/materialize"; Deno.test("takeUntil should return when notifier nexts", () => { // Arrange diff --git a/common/take-until.ts b/take-until/mod.ts similarity index 58% rename from common/take-until.ts rename to take-until/mod.ts index 6743157..6c29cd4 100644 --- a/common/take-until.ts +++ b/take-until/mod.ts @@ -1,16 +1,16 @@ -import { isObservable, Observable } from "@xan/observable-core"; -import { MinimumArgumentsRequiredError, noop, ParameterTypeError } from "@xan/observable-internal"; -import { pipe } from "./pipe.ts"; -import { asObservable } from "./as-observable.ts"; +import { isObservable, Observable, toObservable } from "@observable/core"; +import { MinimumArgumentsRequiredError, noop, ParameterTypeError } from "@observable/internal"; /** - * Takes [`next`](https://jsr.io/@xan/observable-core/doc/~/Observer.next)ed values from the - * [source](https://jsr.io/@xan/observable-core#source) until [notified](https://jsr.io/@xan/observable-core#notifier) + * Takes [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values from the + * [source](https://jsr.io/@observable/core#source) until [notified](https://jsr.io/@observable/core#notifier) * to not. * @example * ```ts - * import { Subject } from "@xan/observable-core"; - * import { takeUntil, of, pipe } from "@xan/observable-common"; + * import { Subject } from "@observable/core"; + * import { takeUntil } from "@observable/take-until"; + * import { of } from "@observable/of"; + * import { pipe } from "@observable/pipe"; * * const controller = new AbortController(); * const source = new Subject(); @@ -18,21 +18,16 @@ import { asObservable } from "./as-observable.ts"; * * pipe(source, takeUntil(notifier)).subscribe({ * signal: controller.signal, - * next: (value) => console.log(value), + * next: (value) => console.log("next", value), * return: () => console.log("return"), - * throw: (value) => console.log(value), + * throw: (value) => console.log("throw", value), * }); * - * source.next(1); - * source.next(2); - * notifier.next(); + * source.next(1); // "next" 1 + * source.next(2); // "next" 2 + * notifier.next(); // "return" * source.next(3); * source.return(); - * - * // console output: - * // 1 - * // 2 - * // return * ``` */ export function takeUntil( @@ -40,11 +35,11 @@ export function takeUntil( ): (source: Observable) => Observable { if (arguments.length === 0) throw new MinimumArgumentsRequiredError(); if (!isObservable(notifier)) throw new ParameterTypeError(0, "Observable"); - notifier = pipe(notifier, asObservable()); + notifier = toObservable(notifier); return function takeUntilFn(source) { if (arguments.length === 0) throw new MinimumArgumentsRequiredError(); if (!isObservable(source)) throw new ParameterTypeError(0, "Observable"); - source = pipe(source, asObservable()); + source = toObservable(source); return new Observable((observer) => { notifier.subscribe({ signal: observer.signal, diff --git a/take/README.md b/take/README.md new file mode 100644 index 0000000..c568e8a --- /dev/null +++ b/take/README.md @@ -0,0 +1,29 @@ +# @observable/take + +Takes the first `count` values [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed by +the [source](https://jsr.io/@observable/core#source). + +## Example + +```ts +import { take } from "@observable/take"; +import { of } from "@observable/of"; +import { pipe } from "@observable/pipe"; + +const controller = new AbortController(); +pipe(of([1, 2, 3, 4, 5]), take(2)).subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), + return: () => console.log("return"), + throw: (value) => console.log("throw", value), +}); + +// console output: +// "next" 1 +// "next" 2 +// "return" +``` + +# Glossary And Semantics + +[@observable/core](https://jsr.io/@observable/core#glossary-and-semantics) diff --git a/take/deno.json b/take/deno.json new file mode 100644 index 0000000..359a3bc --- /dev/null +++ b/take/deno.json @@ -0,0 +1,6 @@ +{ + "name": "@observable/take", + "version": "0.1.0", + "license": "MIT", + "exports": "./mod.ts" +} diff --git a/common/take.test.ts b/take/mod.test.ts similarity index 84% rename from common/take.test.ts rename to take/mod.test.ts index 67b5c2f..66bf4d1 100644 --- a/common/take.test.ts +++ b/take/mod.test.ts @@ -1,13 +1,12 @@ import { assertEquals, assertStrictEquals } from "@std/assert"; -import { Observable, Observer, Subject } from "@xan/observable-core"; -import { noop } from "@xan/observable-internal"; -import { empty } from "./empty.ts"; -import { never } from "./never.ts"; -import { of } from "./of.ts"; -import { pipe } from "./pipe.ts"; -import { materialize } from "./materialize.ts"; -import type { ObserverNotification } from "./observer-notification.ts"; -import { take } from "./take.ts"; +import { Observable, Observer, Subject } from "@observable/core"; +import { noop } from "@observable/internal"; +import { empty } from "@observable/empty"; +import { never } from "@observable/never"; +import { of } from "@observable/of"; +import { pipe } from "@observable/pipe"; +import { materialize, type ObserverNotification } from "@observable/materialize"; +import { take } from "./mod.ts"; Deno.test( "take should return an empty observable if the count is equal to 0", diff --git a/common/take.ts b/take/mod.ts similarity index 67% rename from common/take.ts rename to take/mod.ts index 0ede1b9..0183276 100644 --- a/common/take.ts +++ b/take/mod.ts @@ -1,28 +1,28 @@ -import { isObservable, Observable } from "@xan/observable-core"; -import { MinimumArgumentsRequiredError, ParameterTypeError } from "@xan/observable-internal"; -import { empty } from "./empty.ts"; -import { pipe } from "./pipe.ts"; -import { asObservable } from "./as-observable.ts"; +import { isObservable, Observable, toObservable } from "@observable/core"; +import { MinimumArgumentsRequiredError, ParameterTypeError } from "@observable/internal"; +import { empty } from "@observable/empty"; /** - * Takes the first {@linkcode count} values [`next`](https://jsr.io/@xan/observable-core/doc/~/Observer.next)ed - * by the [source](https://jsr.io/@xan/observable-core#source). + * Takes the first {@linkcode count} values [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed + * by the [source](https://jsr.io/@observable/core#source). * @example * ```ts - * import { take, of, pipe } from "@xan/observable-common"; + * import { take } from "@observable/take"; + * import { of } from "@observable/of"; + * import { pipe } from "@observable/pipe"; * * const controller = new AbortController(); * pipe(of([1, 2, 3, 4, 5]), take(2)).subscribe({ * signal: controller.signal, - * next: (value) => console.log(value), + * next: (value) => console.log("next", value), * return: () => console.log("return"), - * throw: (value) => console.log(value), + * throw: (value) => console.log("throw", value), * }); * * // console output: - * // 1 - * // 2 - * // return + * // "next" 1 + * // "next" 2 + * // "return" * ``` */ export function take( @@ -32,7 +32,7 @@ export function take( if (arguments.length === 0) throw new MinimumArgumentsRequiredError(); if (!isObservable(source)) throw new ParameterTypeError(0, "Observable"); if (count <= 0 || Number.isNaN(count)) return empty; - source = pipe(source, asObservable()); + source = toObservable(source); if (count === Infinity) return source; return new Observable((observer) => { let seen = 0; diff --git a/tap/README.md b/tap/README.md new file mode 100644 index 0000000..76a8cc8 --- /dev/null +++ b/tap/README.md @@ -0,0 +1,44 @@ +# @observable/tap + +Used to perform side-effects on the [source](https://jsr.io/@observable/core#source). + +## Example + +```ts +import { tap } from "@observable/tap"; +import { of } from "@observable/of"; +import { pipe } from "@observable/pipe"; + +const subscriptionController = new AbortController(); +const tapController = new AbortController(); + +pipe( + of([1, 2, 3]), + tap({ + signal: tapController.signal, + next(value) { + if (value === 2) controller.abort(); + console.log("tap next", value); + }, + return: () => console.log("tap return"), + throw: (value) => console.log("tap throw", value), + }), +).subscribe({ + signal: subscriptionController.signal, + next: (value) => console.log("next", value), + return: () => console.log("return"), + throw: (value) => console.log("throw", value), +}); + +// console output: +// "tap next" 1 +// "next" 1 +// "tap next" 2 +// "next" 2 +// "next" 3 +// "return" +``` + +# Glossary And Semantics + +[@observable/core](https://jsr.io/@observable/core#glossary-and-semantics) diff --git a/tap/deno.json b/tap/deno.json new file mode 100644 index 0000000..a0c3a7e --- /dev/null +++ b/tap/deno.json @@ -0,0 +1,6 @@ +{ + "name": "@observable/tap", + "version": "0.1.0", + "license": "MIT", + "exports": "./mod.ts" +} diff --git a/common/tap.test.ts b/tap/mod.test.ts similarity index 90% rename from common/tap.test.ts rename to tap/mod.test.ts index 8b06b7b..0a5139b 100644 --- a/common/tap.test.ts +++ b/tap/mod.test.ts @@ -1,12 +1,11 @@ import { assertEquals } from "@std/assert"; -import { Observable, Observer } from "@xan/observable-core"; -import { materialize } from "./materialize.ts"; -import type { ObserverNotification } from "./observer-notification.ts"; -import { throwError } from "./throw-error.ts"; -import { pipe } from "./pipe.ts"; -import { tap } from "./tap.ts"; -import { empty } from "./empty.ts"; -import { of } from "./of.ts"; +import { Observable, Observer } from "@observable/core"; +import { materialize, type ObserverNotification } from "@observable/materialize"; +import { throwError } from "@observable/throw-error"; +import { pipe } from "@observable/pipe"; +import { tap } from "./mod.ts"; +import { empty } from "@observable/empty"; +import { of } from "@observable/of"; Deno.test("tap should pump next notifications to the provided observer", () => { // Arrange diff --git a/common/tap.ts b/tap/mod.ts similarity index 81% rename from common/tap.ts rename to tap/mod.ts index 65bbaad..fe86e46 100644 --- a/common/tap.ts +++ b/tap/mod.ts @@ -3,16 +3,17 @@ import { isObserver, Observable, type Observer, + toObservable, toObserver, -} from "@xan/observable-core"; -import { MinimumArgumentsRequiredError, ParameterTypeError } from "@xan/observable-internal"; -import { pipe } from "./pipe.ts"; -import { asObservable } from "./as-observable.ts"; +} from "@observable/core"; +import { MinimumArgumentsRequiredError, ParameterTypeError } from "@observable/internal"; /** - * Used to perform side-effects on the [source](https://jsr.io/@xan/observable-core#source). + * Used to perform side-effects on the [source](https://jsr.io/@observable/core#source). * ```ts - * import { of, pipe, tap } from "@xan/observable-common"; + * import { tap } from "@observable/tap"; + * import { of } from "@observable/of"; + * import { pipe } from "@observable/pipe"; * * const subscriptionController = new AbortController(); * const tapController = new AbortController(); @@ -30,17 +31,17 @@ import { asObservable } from "./as-observable.ts"; * }) * ).subscribe({ * signal: subscriptionController.signal, - * next: (value) => console.log(value), + * next: (value) => console.log("next", value), * return: () => console.log("return"), * throw: (value) => console.log("throw", value), * }); * * // console output: - * // tap next 1 - * // 1 - * // tap next 2 - * // 2 - * // 3 + * // "tap next" 1 + * // "next" 1 + * // "tap next" 2 + * // "next" 2 + * // "next" 3 * // "return" * ``` */ @@ -53,7 +54,7 @@ export function tap( return function tapFn(source) { if (arguments.length === 0) throw new MinimumArgumentsRequiredError(); if (!isObservable(source)) throw new ParameterTypeError(0, "Observable"); - source = pipe(source, asObservable()); + source = toObservable(source); return new Observable((observer) => source.subscribe({ signal: observer.signal, diff --git a/throw-error/README.md b/throw-error/README.md new file mode 100644 index 0000000..42b91c8 --- /dev/null +++ b/throw-error/README.md @@ -0,0 +1,24 @@ +# @observable/throw-error + +Creates an [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) that will +[`throw`](https://jsr.io/@observable/core/doc/~/Observer.throw) the given value immediately upon +[`subscribe`](https://jsr.io/@observable/core/doc/~/Observable.subscribe). + +## Example + +```ts +import { throwError } from "@observable/throw-error"; + +const controller = new AbortController(); + +throwError(new Error("throw")).subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), // Never called + return: () => console.log("return"), // Never called + throw: (value) => console.log("throw", value), // Called immediately +}); +``` + +# Glossary And Semantics + +[@observable/core](https://jsr.io/@observable/core#glossary-and-semantics) diff --git a/throw-error/deno.json b/throw-error/deno.json new file mode 100644 index 0000000..f966363 --- /dev/null +++ b/throw-error/deno.json @@ -0,0 +1,6 @@ +{ + "name": "@observable/throw-error", + "version": "0.1.0", + "license": "MIT", + "exports": "./mod.ts" +} diff --git a/common/throw-error.test.ts b/throw-error/mod.test.ts similarity index 68% rename from common/throw-error.test.ts rename to throw-error/mod.test.ts index df97c0e..3bccdfb 100644 --- a/common/throw-error.test.ts +++ b/throw-error/mod.test.ts @@ -1,9 +1,8 @@ -import { Observer } from "@xan/observable-core"; -import { throwError } from "./throw-error.ts"; +import { Observer } from "@observable/core"; +import { throwError } from "./mod.ts"; import { assertEquals } from "@std/assert"; -import { materialize } from "./materialize.ts"; -import type { ObserverNotification } from "./observer-notification.ts"; -import { pipe } from "./pipe.ts"; +import { materialize, type ObserverNotification } from "@observable/materialize"; +import { pipe } from "@observable/pipe"; Deno.test( "throwError should push an error to the observer immediately upon subscription", diff --git a/throw-error/mod.ts b/throw-error/mod.ts new file mode 100644 index 0000000..43bbea8 --- /dev/null +++ b/throw-error/mod.ts @@ -0,0 +1,24 @@ +import { Observable } from "@observable/core"; + +/** + * Creates an [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) that will [`throw`](https://jsr.io/@observable/core/doc/~/Observer.throw) the + * given `value` immediately upon [`subscribe`](https://jsr.io/@observable/core/doc/~/Observable.subscribe). + * + * @param value The value to [`throw`](https://jsr.io/@observable/core/doc/~/Observer.throw). + * @example + * ```ts + * import { throwError } from "@observable/throw-error"; + * + * const controller = new AbortController(); + * + * throwError(new Error("throw")).subscribe({ + * signal: controller.signal, + * next: (value) => console.log("next", value), // Never called + * return: () => console.log("return"), // Never called + * throw: (value) => console.log("throw", value), // Called immediately + * }); + * ``` + */ +export function throwError(value: unknown): Observable { + return new Observable((observer) => observer.throw(value)); +} diff --git a/timer/README.md b/timer/README.md new file mode 100644 index 0000000..3d3cc04 --- /dev/null +++ b/timer/README.md @@ -0,0 +1,64 @@ +# @observable/timer + +Creates an [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) that +[`next`](https://jsr.io/@observable/core/doc/~/Observer.next)s a `0` value after a specified number +of milliseconds and then [`return`](https://jsr.io/@observable/core/doc/~/Observer.return)s. + +## Example + +```ts +import { timer } from "@observable/timer"; + +const controller = new AbortController(); +timer(1_000).subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), + return: () => console.log("return"), + throw: (value) => console.log("throw", value), +}); + +// Console output (after 1 second): +// "next" 0 +// "return" +``` + +## Synchronous completion with 0ms + +```ts +import { timer } from "@observable/timer"; + +const controller = new AbortController(); +timer(0).subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), + return: () => console.log("return"), + throw: (value) => console.log("throw", value), +}); + +// Console output (synchronously): +// "next" 0 +// "return" +``` + +## Edge cases + +```ts +import { timer } from "@observable/timer"; + +const controller = new AbortController(); + +// Negative values return immediately +timer(-1).subscribe({ + signal: controller.signal, + next: (value) => console.log("next", value), + return: () => console.log("return"), + throw: (value) => console.log("throw", value), +}); + +// Console output (synchronously): +// "return" +``` + +# Glossary And Semantics + +[@observable/core](https://jsr.io/@observable/core#glossary-and-semantics) diff --git a/timer/deno.json b/timer/deno.json new file mode 100644 index 0000000..d4b4275 --- /dev/null +++ b/timer/deno.json @@ -0,0 +1,6 @@ +{ + "name": "@observable/timer", + "version": "0.1.0", + "license": "MIT", + "exports": "./mod.ts" +} diff --git a/common/timer.test.ts b/timer/mod.test.ts similarity index 95% rename from common/timer.test.ts rename to timer/mod.test.ts index 123c94b..cff8674 100644 --- a/common/timer.test.ts +++ b/timer/mod.test.ts @@ -1,11 +1,10 @@ import { assertEquals, assertInstanceOf, assertStrictEquals, assertThrows } from "@std/assert"; -import { empty } from "./empty.ts"; -import { never } from "./never.ts"; -import { Observer } from "@xan/observable-core"; -import { timer } from "./timer.ts"; -import type { ObserverNotification } from "./observer-notification.ts"; -import { pipe } from "./pipe.ts"; -import { materialize } from "./materialize.ts"; +import { empty } from "@observable/empty"; +import { never } from "@observable/never"; +import { Observer } from "@observable/core"; +import { timer } from "./mod.ts"; +import { pipe } from "@observable/pipe"; +import { materialize, type ObserverNotification } from "@observable/materialize"; Deno.test("timer should return never if the milliseconds is Infinity", () => { // Arrange diff --git a/common/timer.ts b/timer/mod.ts similarity index 77% rename from common/timer.ts rename to timer/mod.ts index f91bd39..68dac0a 100644 --- a/common/timer.ts +++ b/timer/mod.ts @@ -1,9 +1,9 @@ -import { MinimumArgumentsRequiredError, ParameterTypeError } from "@xan/observable-internal"; -import { Observable } from "@xan/observable-core"; -import { empty } from "./empty.ts"; -import { of } from "./of.ts"; -import { never } from "./never.ts"; -import { flat } from "./flat.ts"; +import { MinimumArgumentsRequiredError, ParameterTypeError } from "@observable/internal"; +import { Observable } from "@observable/core"; +import { empty } from "@observable/empty"; +import { of } from "@observable/of"; +import { never } from "@observable/never"; +import { flat } from "@observable/flat"; /** * @internal Do NOT export. @@ -11,13 +11,13 @@ import { flat } from "./flat.ts"; const success = of([0]); /** - * Creates an [`Observable`](https://jsr.io/@xan/observable-core/doc/~/Observable) that - * [`next`](https://jsr.io/@xan/observable-core/doc/~/Observer.next)s a `0` value after a + * Creates an [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) that + * [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)s a `0` value after a * specified number of {@linkcode milliseconds} and then - * [`return`](https://jsr.io/@xan/observable-core/doc/~/Observer.return)s. + * [`return`](https://jsr.io/@observable/core/doc/~/Observer.return)s. * @example * ```ts - * import { timer } from "@xan/observable-common"; + * import { timer } from "@observable/timer"; * * const controller = new AbortController(); * timer(1_000).subscribe({ @@ -33,7 +33,7 @@ const success = of([0]); * ``` * @example * ```ts - * import { timer } from "@xan/observable-common"; + * import { timer } from "@observable/timer"; * * const controller = new AbortController(); * timer(0).subscribe({ @@ -49,7 +49,7 @@ const success = of([0]); * ``` * @example * ```ts - * import { timer } from "@xan/observable-common"; + * import { timer } from "@observable/timer"; * * const controller = new AbortController(); * timer(-1).subscribe({ @@ -64,7 +64,7 @@ const success = of([0]); * ``` * @example * ```ts - * import { timer } from "@xan/observable-common"; + * import { timer } from "@observable/timer"; * * const controller = new AbortController(); * timer(NaN).subscribe({ diff --git a/web/README.md b/web/README.md deleted file mode 100644 index fd56794..0000000 --- a/web/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# @xan/observable-web - -[@xan/observable-core](https://jsr.io/@xan/observable-core) -[Web APIs](https://developer.mozilla.org/en-US/docs/Web/API) utilities. diff --git a/web/broadcast-subject-constructor.ts b/web/broadcast-subject-constructor.ts deleted file mode 100644 index aef34e5..0000000 --- a/web/broadcast-subject-constructor.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { BroadcastSubject } from "./broadcast-subject.ts"; - -/** - * Object interface for an {@linkcode BroadcastSubject} factory. - */ -export interface BroadcastSubjectConstructor { - /** - * Creates and returns a variant of [`Subject`](https://jsr.io/@xan/subject/doc/~/Subject). When values - * are [`next`](https://jsr.io/@xan/observable-core/doc/~/Observer.next)ed, they are - * {@linkcode structuredClone|structured cloned} and sent only to [consumers](https://jsr.io/@xan/observable-core#consumer) - * of _other_ {@linkcode BroadcastSubject} instances with the same {@linkcode name} even if they are in different browsing - * contexts (e.g. browser tabs). Logically, [consumers](https://jsr.io/@xan/observable-core#consumer) of the - * {@linkcode BroadcastSubject} do not receive it's _own_ - * [`next`](https://jsr.io/@xan/observable-core/doc/~/Observer.next)ed values. - * @example - * ```ts - * import { BroadcastSubject } from "@xan/observable-web"; - * - * // Setup subjects - * const name = "test"; - * const controller = new AbortController(); - * const subject1 = new BroadcastSubject(name); - * const subject2 = new BroadcastSubject(name); - * - * // Subscribe to subjects - * subject1.subscribe({ - * signal: controller.signal, - * next: (value) => console.log("subject1 received", value, "from subject1"), - * return: () => console.log("subject1 returned"), - * throw: (value) => console.log("subject1 threw", value), - * }); - * subject2.subscribe({ - * signal: controller.signal, - * next: (value) => console.log("subject2 received", value, "from subject2"), - * return: () => console.log("subject2 returned"), - * throw: (value) => console.log("subject2 threw", value), - * }); - * - * subject1.next(1); // subject2 received 1 from subject1 - * subject2.next(2); // subject1 received 2 from subject2 - * subject2.return(); // subject2 returned - * subject1.next(3); // No console output since subject2 is already returned - * ``` - */ - new (name: string): BroadcastSubject; - new (name: string): BroadcastSubject; - readonly prototype: BroadcastSubject; -} diff --git a/web/mod.test.ts b/web/mod.test.ts deleted file mode 100644 index 447c3b9..0000000 --- a/web/mod.test.ts +++ /dev/null @@ -1,4 +0,0 @@ -Deno.test("mod should be importable", async () => { - // Arrange / Act / Assert - await import("./mod.ts"); -}); diff --git a/web/mod.ts b/web/mod.ts deleted file mode 100644 index 0273398..0000000 --- a/web/mod.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type { BroadcastSubjectConstructor } from "./broadcast-subject-constructor.ts"; -export { BroadcastSubject } from "./broadcast-subject.ts"; From 03072459da3714aca0fb136a749db2df9757a238 Mon Sep 17 00:00:00 2001 From: Alexander Harding Date: Fri, 16 Jan 2026 21:01:25 -0700 Subject: [PATCH 2/4] Update BroadcastSubject documentation for clarity and accuracy - Refined the description of the BroadcastSubjectConstructor to enhance clarity. - Improved links to relevant documentation and resources. - Corrected minor grammatical issues for better readability. --- broadcast-subject/mod.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/broadcast-subject/mod.ts b/broadcast-subject/mod.ts index efde284..f2196f4 100644 --- a/broadcast-subject/mod.ts +++ b/broadcast-subject/mod.ts @@ -15,12 +15,10 @@ export type BroadcastSubject = Subject; */ export interface BroadcastSubjectConstructor { /** - * Creates and returns a variant of [`Subject`](https://jsr.io/@xan/subject/doc/~/Subject). When values - * are [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed, they are - * {@linkcode structuredClone|structured cloned} and sent only to [consumers](https://jsr.io/@observable/core#consumer) - * of _other_ {@linkcode BroadcastSubject} instances with the same {@linkcode name} even if they are in different browsing - * contexts (e.g. browser tabs). Logically, [consumers](https://jsr.io/@observable/core#consumer) of the - * {@linkcode BroadcastSubject} do not receive it's _own_ + * Creates and returns a variant of [`Subject`](https://jsr.io/@xan/subject/doc/~/Subject) whose [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed + * values are [`structured cloned`](https://developer.mozilla.org/en-US/docs/Web/API/structuredClone) and sent only to [consumers](https://jsr.io/@observable/core#consumer) + * of _other_ {@linkcode BroadcastSubject} instances with the same {@linkcode name} even if they are in different browsing contexts (e.g. browser tabs). Logically, + * [consumers](https://jsr.io/@observable/core#consumer) of the {@linkcode BroadcastSubject} do not receive it's _own_ * [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values. * @example * ```ts From 1f7421ad1e7bd2cf880d1e63a524c1f5fc72530f Mon Sep 17 00:00:00 2001 From: Alexander Harding Date: Fri, 16 Jan 2026 21:07:29 -0700 Subject: [PATCH 3/4] Enhance README documentation across multiple modules - Added sections for Build, Publishing, and Running unit tests to README files for consistency. - Updated build automation references to Deno across all relevant modules. - Ensured clarity and completeness in documentation for better user guidance. --- all/README.md | 13 +++++++++++++ async-subject/README.md | 13 +++++++++++++ behavior-subject/README.md | 13 +++++++++++++ broadcast-subject/README.md | 13 +++++++++++++ core/README.md | 2 +- defer/README.md | 13 +++++++++++++ drop/README.md | 13 +++++++++++++ empty/README.md | 13 +++++++++++++ exhaust-map/README.md | 13 +++++++++++++ filter/README.md | 13 +++++++++++++ finalize/README.md | 13 +++++++++++++ flat-map/README.md | 13 +++++++++++++ flat/README.md | 13 +++++++++++++ ignore-elements/README.md | 13 +++++++++++++ internal/README.md | 13 +++++++++++++ map/README.md | 13 +++++++++++++ materialize/README.md | 13 +++++++++++++ merge-map/README.md | 13 +++++++++++++ merge/README.md | 13 +++++++++++++ never/README.md | 13 +++++++++++++ of/README.md | 13 +++++++++++++ pipe/README.md | 13 +++++++++++++ race/README.md | 13 +++++++++++++ replay-subject/README.md | 13 +++++++++++++ switch-map/README.md | 13 +++++++++++++ take-until/README.md | 13 +++++++++++++ take/README.md | 13 +++++++++++++ tap/README.md | 13 +++++++++++++ throw-error/README.md | 13 +++++++++++++ timer/README.md | 13 +++++++++++++ 30 files changed, 378 insertions(+), 1 deletion(-) diff --git a/all/README.md b/all/README.md index ffaa57e..ff4e246 100644 --- a/all/README.md +++ b/all/README.md @@ -7,6 +7,19 @@ latest [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values of [sources](https://jsr.io/@observable/core#source) are empty, the returned [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) will also be empty. +## Build + +Automated by [Deno](https://deno.land/) + +## Publishing + +Automated by `.github\workflows\publish.yml`. + +## Running unit tests + +Run `deno task test` or `deno task test:ci` to execute the unit tests via +[Deno](https://deno.land/). + ## Example ```ts diff --git a/async-subject/README.md b/async-subject/README.md index 9815f25..97567a8 100644 --- a/async-subject/README.md +++ b/async-subject/README.md @@ -8,6 +8,19 @@ A variant of [`Subject`](https://jsr.io/@observable/core/doc/~/Subject) that buf [`consumers`](https://jsr.io/@observable/core#consumer) upon [`subscription`](https://jsr.io/@observable/core/doc/~/Observable.subscribe). +## Build + +Automated by [Deno](https://deno.land/) + +## Publishing + +Automated by `.github\workflows\publish.yml`. + +## Running unit tests + +Run `deno task test` or `deno task test:ci` to execute the unit tests via +[Deno](https://deno.land/). + ## Example ```ts diff --git a/behavior-subject/README.md b/behavior-subject/README.md index 41ce7a8..3ee65ce 100644 --- a/behavior-subject/README.md +++ b/behavior-subject/README.md @@ -4,6 +4,19 @@ A variant of [`Subject`](https://jsr.io/@observable/core/doc/~/Subject) that kee current value and replays it to [`consumers`](https://jsr.io/@observable/core#consumer) upon [`subscription`](https://jsr.io/@observable/core/doc/~/Observable.subscribe). +## Build + +Automated by [Deno](https://deno.land/) + +## Publishing + +Automated by `.github\workflows\publish.yml`. + +## Running unit tests + +Run `deno task test` or `deno task test:ci` to execute the unit tests via +[Deno](https://deno.land/). + ## Example ```ts diff --git a/broadcast-subject/README.md b/broadcast-subject/README.md index aa75777..0190ab4 100644 --- a/broadcast-subject/README.md +++ b/broadcast-subject/README.md @@ -8,6 +8,19 @@ with the same name even if they are in different browsing contexts (e.g. browser [consumers](https://jsr.io/@observable/core#consumer) of the `BroadcastSubject` do not receive its _own_ [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values. +## Build + +Automated by [Deno](https://deno.land/) + +## Publishing + +Automated by `.github\workflows\publish.yml`. + +## Running unit tests + +Run `deno task test` or `deno task test:ci` to execute the unit tests via +[Deno](https://deno.land/). + ## Example ```ts diff --git a/core/README.md b/core/README.md index 4ca4768..7295f6f 100644 --- a/core/README.md +++ b/core/README.md @@ -9,7 +9,7 @@ synchronous and asynchronous [producers](#producer). ## Build -Automated by [JSR](https://jsr.io/) +Automated by [Deno](https://deno.land/) ## Publishing diff --git a/defer/README.md b/defer/README.md index 7c5a520..9947f9d 100644 --- a/defer/README.md +++ b/defer/README.md @@ -6,6 +6,19 @@ Creates an [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) that [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) for each [`Observer`](https://jsr.io/@observable/core/doc/~/Observer). +## Build + +Automated by [Deno](https://deno.land/) + +## Publishing + +Automated by `.github\workflows\publish.yml`. + +## Running unit tests + +Run `deno task test` or `deno task test:ci` to execute the unit tests via +[Deno](https://deno.land/). + ## Example ```ts diff --git a/drop/README.md b/drop/README.md index 01645b5..4ccbd7c 100644 --- a/drop/README.md +++ b/drop/README.md @@ -3,6 +3,19 @@ Drops the first `count` values [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed by the [source](https://jsr.io/@observable/core#source). +## Build + +Automated by [Deno](https://deno.land/) + +## Publishing + +Automated by `.github\workflows\publish.yml`. + +## Running unit tests + +Run `deno task test` or `deno task test:ci` to execute the unit tests via +[Deno](https://deno.land/). + ## Example ```ts diff --git a/empty/README.md b/empty/README.md index a6baab7..f05600d 100644 --- a/empty/README.md +++ b/empty/README.md @@ -4,6 +4,19 @@ An [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) that calls [`return`](https://jsr.io/@observable/core/doc/~/Observer.return) immediately on [`subscribe`](https://jsr.io/@observable/core/doc/~/Observable.subscribe). +## Build + +Automated by [Deno](https://deno.land/) + +## Publishing + +Automated by `.github\workflows\publish.yml`. + +## Running unit tests + +Run `deno task test` or `deno task test:ci` to execute the unit tests via +[Deno](https://deno.land/). + ## Example ```ts diff --git a/exhaust-map/README.md b/exhaust-map/README.md index 579b23f..e12d29c 100644 --- a/exhaust-map/README.md +++ b/exhaust-map/README.md @@ -6,6 +6,19 @@ Projects each [source](https://jsr.io/@observable/core#source) value to an [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) has [`return`](https://jsr.io/@observable/core/doc/~/Observer.return)ed. +## Build + +Automated by [Deno](https://deno.land/) + +## Publishing + +Automated by `.github\workflows\publish.yml`. + +## Running unit tests + +Run `deno task test` or `deno task test:ci` to execute the unit tests via +[Deno](https://deno.land/). + ## Example ```ts diff --git a/filter/README.md b/filter/README.md index ac449c0..e8596c4 100644 --- a/filter/README.md +++ b/filter/README.md @@ -3,6 +3,19 @@ Filters [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values from the [source](https://jsr.io/@observable/core#source) that satisfy a specified predicate. +## Build + +Automated by [Deno](https://deno.land/) + +## Publishing + +Automated by `.github\workflows\publish.yml`. + +## Running unit tests + +Run `deno task test` or `deno task test:ci` to execute the unit tests via +[Deno](https://deno.land/). + ## Example ```ts diff --git a/finalize/README.md b/finalize/README.md index b68161c..a4c1cd4 100644 --- a/finalize/README.md +++ b/finalize/README.md @@ -8,6 +8,19 @@ send no more values. Finalization, if it occurs, will always happen as a side-ef [`throw`](https://jsr.io/@observable/core/doc/~/Observer.throw), or [`unsubscribe`](https://jsr.io/@observable/core/doc/~/Observer.signal) (whichever comes last). +## Build + +Automated by [Deno](https://deno.land/) + +## Publishing + +Automated by `.github\workflows\publish.yml`. + +## Running unit tests + +Run `deno task test` or `deno task test:ci` to execute the unit tests via +[Deno](https://deno.land/). + ## Example ```ts diff --git a/flat-map/README.md b/flat-map/README.md index 9773477..e206b6e 100644 --- a/flat-map/README.md +++ b/flat-map/README.md @@ -6,6 +6,19 @@ Projects each [source](https://jsr.io/@observable/core#source) value to an for each one to [`return`](https://jsr.io/@observable/core/doc/~/Observer.return) before merging the next. +## Build + +Automated by [Deno](https://deno.land/) + +## Publishing + +Automated by `.github\workflows\publish.yml`. + +## Running unit tests + +Run `deno task test` or `deno task test:ci` to execute the unit tests via +[Deno](https://deno.land/). + ## Example ```ts diff --git a/flat/README.md b/flat/README.md index 53cf919..0da9ddd 100644 --- a/flat/README.md +++ b/flat/README.md @@ -4,6 +4,19 @@ Creates an [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) whic all values from the first given [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) and then moves on to the next. +## Build + +Automated by [Deno](https://deno.land/) + +## Publishing + +Automated by `.github\workflows\publish.yml`. + +## Running unit tests + +Run `deno task test` or `deno task test:ci` to execute the unit tests via +[Deno](https://deno.land/). + ## Example ```ts diff --git a/ignore-elements/README.md b/ignore-elements/README.md index e45589a..a34fc10 100644 --- a/ignore-elements/README.md +++ b/ignore-elements/README.md @@ -3,6 +3,19 @@ Ignores all [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values from the [source](https://jsr.io/@observable/core#source). +## Build + +Automated by [Deno](https://deno.land/) + +## Publishing + +Automated by `.github\workflows\publish.yml`. + +## Running unit tests + +Run `deno task test` or `deno task test:ci` to execute the unit tests via +[Deno](https://deno.land/). + ## Example ```ts diff --git a/internal/README.md b/internal/README.md index 364e307..e4cd9a6 100644 --- a/internal/README.md +++ b/internal/README.md @@ -2,3 +2,16 @@ Internal utilities for the [@observable](https://jsr.io/@observable) libraries. Do NOT depend on this library directly as it's an internal implementation detail. + +## Build + +Automated by [Deno](https://deno.land/) + +## Publishing + +Automated by `.github\workflows\publish.yml`. + +## Running unit tests + +Run `deno task test` or `deno task test:ci` to execute the unit tests via +[Deno](https://deno.land/). diff --git a/map/README.md b/map/README.md index 5966a7a..448540a 100644 --- a/map/README.md +++ b/map/README.md @@ -2,6 +2,19 @@ Projects each value from the [source](https://jsr.io/@observable/core#source) to a new value. +## Build + +Automated by [Deno](https://deno.land/) + +## Publishing + +Automated by `.github\workflows\publish.yml`. + +## Running unit tests + +Run `deno task test` or `deno task test:ci` to execute the unit tests via +[Deno](https://deno.land/). + ## Example ```ts diff --git a/materialize/README.md b/materialize/README.md index b76c109..6037e13 100644 --- a/materialize/README.md +++ b/materialize/README.md @@ -4,6 +4,19 @@ Represents all of the notifications from the [source](https://jsr.io/@observable [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values marked with their original types within notification entries. This is especially useful for testing, debugging, and logging. +## Build + +Automated by [Deno](https://deno.land/) + +## Publishing + +Automated by `.github\workflows\publish.yml`. + +## Running unit tests + +Run `deno task test` or `deno task test:ci` to execute the unit tests via +[Deno](https://deno.land/). + ## Example ```ts diff --git a/merge-map/README.md b/merge-map/README.md index 7a97d67..7348415 100644 --- a/merge-map/README.md +++ b/merge-map/README.md @@ -4,6 +4,19 @@ Projects each [source](https://jsr.io/@observable/core#source) value to an [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) which is merged in the output [`Observable`](https://jsr.io/@observable/core/doc/~/Observable). +## Build + +Automated by [Deno](https://deno.land/) + +## Publishing + +Automated by `.github\workflows\publish.yml`. + +## Running unit tests + +Run `deno task test` or `deno task test:ci` to execute the unit tests via +[Deno](https://deno.land/). + ## Example ```ts diff --git a/merge/README.md b/merge/README.md index ed5be28..6479b35 100644 --- a/merge/README.md +++ b/merge/README.md @@ -4,6 +4,19 @@ Creates and returns an [`Observable`](https://jsr.io/@observable/core/doc/~/Obse concurrently [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)s all values from every given [source](https://jsr.io/@observable/core#source). +## Build + +Automated by [Deno](https://deno.land/) + +## Publishing + +Automated by `.github\workflows\publish.yml`. + +## Running unit tests + +Run `deno task test` or `deno task test:ci` to execute the unit tests via +[Deno](https://deno.land/). + ## Example ```ts diff --git a/never/README.md b/never/README.md index 01c29ee..e71b7fb 100644 --- a/never/README.md +++ b/never/README.md @@ -3,6 +3,19 @@ An [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) that does nothing on [`subscribe`](https://jsr.io/@observable/core/doc/~/Observable.subscribe). +## Build + +Automated by [Deno](https://deno.land/) + +## Publishing + +Automated by `.github\workflows\publish.yml`. + +## Running unit tests + +Run `deno task test` or `deno task test:ci` to execute the unit tests via +[Deno](https://deno.land/). + ## Example ```ts diff --git a/of/README.md b/of/README.md index 2d3322f..c96a16f 100644 --- a/of/README.md +++ b/of/README.md @@ -5,6 +5,19 @@ sequence of values in order on [`subscribe`](https://jsr.io/@observable/core/doc/~/Observable.subscribe) and then [`return`](https://jsr.io/@observable/core/doc/~/Observer.return)s. +## Build + +Automated by [Deno](https://deno.land/) + +## Publishing + +Automated by `.github\workflows\publish.yml`. + +## Running unit tests + +Run `deno task test` or `deno task test:ci` to execute the unit tests via +[Deno](https://deno.land/). + ## Example ```ts diff --git a/pipe/README.md b/pipe/README.md index af384ad..ef2dd78 100644 --- a/pipe/README.md +++ b/pipe/README.md @@ -2,6 +2,19 @@ Pipe a value through a series of unary functions. +## Build + +Automated by [Deno](https://deno.land/) + +## Publishing + +Automated by `.github\workflows\publish.yml`. + +## Running unit tests + +Run `deno task test` or `deno task test:ci` to execute the unit tests via +[Deno](https://deno.land/). + ## Example ```ts diff --git a/race/README.md b/race/README.md index e83705f..899939f 100644 --- a/race/README.md +++ b/race/README.md @@ -5,6 +5,19 @@ the first [source](https://jsr.io/@observable/core#source) to [`next`](https://jsr.io/@observable/core/doc/~/Observer.next) or [`throw`](https://jsr.io/@observable/core/doc/~/Observer.throw) a value. +## Build + +Automated by [Deno](https://deno.land/) + +## Publishing + +Automated by `.github\workflows\publish.yml`. + +## Running unit tests + +Run `deno task test` or `deno task test:ci` to execute the unit tests via +[Deno](https://deno.land/). + ## Example ```ts diff --git a/replay-subject/README.md b/replay-subject/README.md index a94c90d..5a743ab 100644 --- a/replay-subject/README.md +++ b/replay-subject/README.md @@ -4,6 +4,19 @@ A variant of [`Subject`](https://jsr.io/@observable/core/doc/~/Subject) that rep [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values upon [`subscription`](https://jsr.io/@observable/core/doc/~/Observable.subscribe). +## Build + +Automated by [Deno](https://deno.land/) + +## Publishing + +Automated by `.github\workflows\publish.yml`. + +## Running unit tests + +Run `deno task test` or `deno task test:ci` to execute the unit tests via +[Deno](https://deno.land/). + ## Example ```ts diff --git a/switch-map/README.md b/switch-map/README.md index a1d22dd..fb18fa7 100644 --- a/switch-map/README.md +++ b/switch-map/README.md @@ -5,6 +5,19 @@ Projects each [source](https://jsr.io/@observable/core#source) value to an [`Observable`](https://jsr.io/@observable/core/doc/~/Observable), emitting values only from the most recently projected [`Observable`](https://jsr.io/@observable/core/doc/~/Observable). +## Build + +Automated by [Deno](https://deno.land/) + +## Publishing + +Automated by `.github\workflows\publish.yml`. + +## Running unit tests + +Run `deno task test` or `deno task test:ci` to execute the unit tests via +[Deno](https://deno.land/). + ## Example ```ts diff --git a/take-until/README.md b/take-until/README.md index 7d24ead..0732037 100644 --- a/take-until/README.md +++ b/take-until/README.md @@ -4,6 +4,19 @@ Takes [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values fro [source](https://jsr.io/@observable/core#source) until [notified](https://jsr.io/@observable/core#notifier) to stop. +## Build + +Automated by [Deno](https://deno.land/) + +## Publishing + +Automated by `.github\workflows\publish.yml`. + +## Running unit tests + +Run `deno task test` or `deno task test:ci` to execute the unit tests via +[Deno](https://deno.land/). + ## Example ```ts diff --git a/take/README.md b/take/README.md index c568e8a..ffe7559 100644 --- a/take/README.md +++ b/take/README.md @@ -3,6 +3,19 @@ Takes the first `count` values [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed by the [source](https://jsr.io/@observable/core#source). +## Build + +Automated by [Deno](https://deno.land/) + +## Publishing + +Automated by `.github\workflows\publish.yml`. + +## Running unit tests + +Run `deno task test` or `deno task test:ci` to execute the unit tests via +[Deno](https://deno.land/). + ## Example ```ts diff --git a/tap/README.md b/tap/README.md index 76a8cc8..8923ca1 100644 --- a/tap/README.md +++ b/tap/README.md @@ -2,6 +2,19 @@ Used to perform side-effects on the [source](https://jsr.io/@observable/core#source). +## Build + +Automated by [Deno](https://deno.land/) + +## Publishing + +Automated by `.github\workflows\publish.yml`. + +## Running unit tests + +Run `deno task test` or `deno task test:ci` to execute the unit tests via +[Deno](https://deno.land/). + ## Example ```ts diff --git a/throw-error/README.md b/throw-error/README.md index 42b91c8..672a2f9 100644 --- a/throw-error/README.md +++ b/throw-error/README.md @@ -4,6 +4,19 @@ Creates an [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) that [`throw`](https://jsr.io/@observable/core/doc/~/Observer.throw) the given value immediately upon [`subscribe`](https://jsr.io/@observable/core/doc/~/Observable.subscribe). +## Build + +Automated by [Deno](https://deno.land/) + +## Publishing + +Automated by `.github\workflows\publish.yml`. + +## Running unit tests + +Run `deno task test` or `deno task test:ci` to execute the unit tests via +[Deno](https://deno.land/). + ## Example ```ts diff --git a/timer/README.md b/timer/README.md index 3d3cc04..3f5e5d8 100644 --- a/timer/README.md +++ b/timer/README.md @@ -4,6 +4,19 @@ Creates an [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) that [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)s a `0` value after a specified number of milliseconds and then [`return`](https://jsr.io/@observable/core/doc/~/Observer.return)s. +## Build + +Automated by [Deno](https://deno.land/) + +## Publishing + +Automated by `.github\workflows\publish.yml`. + +## Running unit tests + +Run `deno task test` or `deno task test:ci` to execute the unit tests via +[Deno](https://deno.land/). + ## Example ```ts From 55217050d21727433518351091a359211fbbcfe1 Mon Sep 17 00:00:00 2001 From: Alexander Harding Date: Fri, 16 Jan 2026 21:09:34 -0700 Subject: [PATCH 4/4] Update README files to reflect automation changes - Replaced references to Deno with JSR for build automation across all modules. - Ensured consistency in documentation for improved clarity and user guidance. --- all/README.md | 2 +- async-subject/README.md | 2 +- behavior-subject/README.md | 2 +- broadcast-subject/README.md | 2 +- core/README.md | 2 +- defer/README.md | 2 +- drop/README.md | 2 +- empty/README.md | 2 +- exhaust-map/README.md | 2 +- filter/README.md | 2 +- finalize/README.md | 2 +- flat-map/README.md | 2 +- flat/README.md | 2 +- ignore-elements/README.md | 2 +- internal/README.md | 2 +- map/README.md | 2 +- materialize/README.md | 2 +- merge-map/README.md | 2 +- merge/README.md | 2 +- never/README.md | 2 +- of/README.md | 2 +- pipe/README.md | 2 +- race/README.md | 2 +- replay-subject/README.md | 2 +- switch-map/README.md | 2 +- take-until/README.md | 2 +- take/README.md | 2 +- tap/README.md | 2 +- throw-error/README.md | 2 +- timer/README.md | 2 +- 30 files changed, 30 insertions(+), 30 deletions(-) diff --git a/all/README.md b/all/README.md index ff4e246..e038fe4 100644 --- a/all/README.md +++ b/all/README.md @@ -9,7 +9,7 @@ latest [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values of ## Build -Automated by [Deno](https://deno.land/) +Automated by [JSR](https://jsr.io/) ## Publishing diff --git a/async-subject/README.md b/async-subject/README.md index 97567a8..9043875 100644 --- a/async-subject/README.md +++ b/async-subject/README.md @@ -10,7 +10,7 @@ A variant of [`Subject`](https://jsr.io/@observable/core/doc/~/Subject) that buf ## Build -Automated by [Deno](https://deno.land/) +Automated by [JSR](https://jsr.io/) ## Publishing diff --git a/behavior-subject/README.md b/behavior-subject/README.md index 3ee65ce..4650af1 100644 --- a/behavior-subject/README.md +++ b/behavior-subject/README.md @@ -6,7 +6,7 @@ current value and replays it to [`consumers`](https://jsr.io/@observable/core#co ## Build -Automated by [Deno](https://deno.land/) +Automated by [JSR](https://jsr.io/) ## Publishing diff --git a/broadcast-subject/README.md b/broadcast-subject/README.md index 0190ab4..21ea4d6 100644 --- a/broadcast-subject/README.md +++ b/broadcast-subject/README.md @@ -10,7 +10,7 @@ _own_ [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values. ## Build -Automated by [Deno](https://deno.land/) +Automated by [JSR](https://jsr.io/) ## Publishing diff --git a/core/README.md b/core/README.md index 7295f6f..4ca4768 100644 --- a/core/README.md +++ b/core/README.md @@ -9,7 +9,7 @@ synchronous and asynchronous [producers](#producer). ## Build -Automated by [Deno](https://deno.land/) +Automated by [JSR](https://jsr.io/) ## Publishing diff --git a/defer/README.md b/defer/README.md index 9947f9d..149d5af 100644 --- a/defer/README.md +++ b/defer/README.md @@ -8,7 +8,7 @@ Creates an [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) that ## Build -Automated by [Deno](https://deno.land/) +Automated by [JSR](https://jsr.io/) ## Publishing diff --git a/drop/README.md b/drop/README.md index 4ccbd7c..dd53bb0 100644 --- a/drop/README.md +++ b/drop/README.md @@ -5,7 +5,7 @@ the [source](https://jsr.io/@observable/core#source). ## Build -Automated by [Deno](https://deno.land/) +Automated by [JSR](https://jsr.io/) ## Publishing diff --git a/empty/README.md b/empty/README.md index f05600d..60c86e6 100644 --- a/empty/README.md +++ b/empty/README.md @@ -6,7 +6,7 @@ An [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) that calls ## Build -Automated by [Deno](https://deno.land/) +Automated by [JSR](https://jsr.io/) ## Publishing diff --git a/exhaust-map/README.md b/exhaust-map/README.md index e12d29c..f25a29e 100644 --- a/exhaust-map/README.md +++ b/exhaust-map/README.md @@ -8,7 +8,7 @@ Projects each [source](https://jsr.io/@observable/core#source) value to an ## Build -Automated by [Deno](https://deno.land/) +Automated by [JSR](https://jsr.io/) ## Publishing diff --git a/filter/README.md b/filter/README.md index e8596c4..4fe5e16 100644 --- a/filter/README.md +++ b/filter/README.md @@ -5,7 +5,7 @@ Filters [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values f ## Build -Automated by [Deno](https://deno.land/) +Automated by [JSR](https://jsr.io/) ## Publishing diff --git a/finalize/README.md b/finalize/README.md index a4c1cd4..788cf95 100644 --- a/finalize/README.md +++ b/finalize/README.md @@ -10,7 +10,7 @@ send no more values. Finalization, if it occurs, will always happen as a side-ef ## Build -Automated by [Deno](https://deno.land/) +Automated by [JSR](https://jsr.io/) ## Publishing diff --git a/flat-map/README.md b/flat-map/README.md index e206b6e..fb52e38 100644 --- a/flat-map/README.md +++ b/flat-map/README.md @@ -8,7 +8,7 @@ next. ## Build -Automated by [Deno](https://deno.land/) +Automated by [JSR](https://jsr.io/) ## Publishing diff --git a/flat/README.md b/flat/README.md index 0da9ddd..7457f27 100644 --- a/flat/README.md +++ b/flat/README.md @@ -6,7 +6,7 @@ then moves on to the next. ## Build -Automated by [Deno](https://deno.land/) +Automated by [JSR](https://jsr.io/) ## Publishing diff --git a/ignore-elements/README.md b/ignore-elements/README.md index a34fc10..4d6f892 100644 --- a/ignore-elements/README.md +++ b/ignore-elements/README.md @@ -5,7 +5,7 @@ Ignores all [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed valu ## Build -Automated by [Deno](https://deno.land/) +Automated by [JSR](https://jsr.io/) ## Publishing diff --git a/internal/README.md b/internal/README.md index e4cd9a6..97b388e 100644 --- a/internal/README.md +++ b/internal/README.md @@ -5,7 +5,7 @@ this library directly as it's an internal implementation detail. ## Build -Automated by [Deno](https://deno.land/) +Automated by [JSR](https://jsr.io/) ## Publishing diff --git a/map/README.md b/map/README.md index 448540a..c36498b 100644 --- a/map/README.md +++ b/map/README.md @@ -4,7 +4,7 @@ Projects each value from the [source](https://jsr.io/@observable/core#source) to ## Build -Automated by [Deno](https://deno.land/) +Automated by [JSR](https://jsr.io/) ## Publishing diff --git a/materialize/README.md b/materialize/README.md index 6037e13..25448d1 100644 --- a/materialize/README.md +++ b/materialize/README.md @@ -6,7 +6,7 @@ types within notification entries. This is especially useful for testing, debugg ## Build -Automated by [Deno](https://deno.land/) +Automated by [JSR](https://jsr.io/) ## Publishing diff --git a/merge-map/README.md b/merge-map/README.md index 7348415..0f7d545 100644 --- a/merge-map/README.md +++ b/merge-map/README.md @@ -6,7 +6,7 @@ Projects each [source](https://jsr.io/@observable/core#source) value to an ## Build -Automated by [Deno](https://deno.land/) +Automated by [JSR](https://jsr.io/) ## Publishing diff --git a/merge/README.md b/merge/README.md index 6479b35..b4f94f5 100644 --- a/merge/README.md +++ b/merge/README.md @@ -6,7 +6,7 @@ given [source](https://jsr.io/@observable/core#source). ## Build -Automated by [Deno](https://deno.land/) +Automated by [JSR](https://jsr.io/) ## Publishing diff --git a/never/README.md b/never/README.md index e71b7fb..28a2ee8 100644 --- a/never/README.md +++ b/never/README.md @@ -5,7 +5,7 @@ An [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) that does no ## Build -Automated by [Deno](https://deno.land/) +Automated by [JSR](https://jsr.io/) ## Publishing diff --git a/of/README.md b/of/README.md index c96a16f..3320122 100644 --- a/of/README.md +++ b/of/README.md @@ -7,7 +7,7 @@ sequence of values in order on ## Build -Automated by [Deno](https://deno.land/) +Automated by [JSR](https://jsr.io/) ## Publishing diff --git a/pipe/README.md b/pipe/README.md index ef2dd78..f80fd55 100644 --- a/pipe/README.md +++ b/pipe/README.md @@ -4,7 +4,7 @@ Pipe a value through a series of unary functions. ## Build -Automated by [Deno](https://deno.land/) +Automated by [JSR](https://jsr.io/) ## Publishing diff --git a/race/README.md b/race/README.md index 899939f..84289f8 100644 --- a/race/README.md +++ b/race/README.md @@ -7,7 +7,7 @@ the first [source](https://jsr.io/@observable/core#source) to ## Build -Automated by [Deno](https://deno.land/) +Automated by [JSR](https://jsr.io/) ## Publishing diff --git a/replay-subject/README.md b/replay-subject/README.md index 5a743ab..2e19644 100644 --- a/replay-subject/README.md +++ b/replay-subject/README.md @@ -6,7 +6,7 @@ A variant of [`Subject`](https://jsr.io/@observable/core/doc/~/Subject) that rep ## Build -Automated by [Deno](https://deno.land/) +Automated by [JSR](https://jsr.io/) ## Publishing diff --git a/switch-map/README.md b/switch-map/README.md index fb18fa7..e3de76d 100644 --- a/switch-map/README.md +++ b/switch-map/README.md @@ -7,7 +7,7 @@ recently projected [`Observable`](https://jsr.io/@observable/core/doc/~/Observab ## Build -Automated by [Deno](https://deno.land/) +Automated by [JSR](https://jsr.io/) ## Publishing diff --git a/take-until/README.md b/take-until/README.md index 0732037..4ff9b33 100644 --- a/take-until/README.md +++ b/take-until/README.md @@ -6,7 +6,7 @@ Takes [`next`](https://jsr.io/@observable/core/doc/~/Observer.next)ed values fro ## Build -Automated by [Deno](https://deno.land/) +Automated by [JSR](https://jsr.io/) ## Publishing diff --git a/take/README.md b/take/README.md index ffe7559..6a2023d 100644 --- a/take/README.md +++ b/take/README.md @@ -5,7 +5,7 @@ the [source](https://jsr.io/@observable/core#source). ## Build -Automated by [Deno](https://deno.land/) +Automated by [JSR](https://jsr.io/) ## Publishing diff --git a/tap/README.md b/tap/README.md index 8923ca1..87ae504 100644 --- a/tap/README.md +++ b/tap/README.md @@ -4,7 +4,7 @@ Used to perform side-effects on the [source](https://jsr.io/@observable/core#sou ## Build -Automated by [Deno](https://deno.land/) +Automated by [JSR](https://jsr.io/) ## Publishing diff --git a/throw-error/README.md b/throw-error/README.md index 672a2f9..76a70e8 100644 --- a/throw-error/README.md +++ b/throw-error/README.md @@ -6,7 +6,7 @@ Creates an [`Observable`](https://jsr.io/@observable/core/doc/~/Observable) that ## Build -Automated by [Deno](https://deno.land/) +Automated by [JSR](https://jsr.io/) ## Publishing diff --git a/timer/README.md b/timer/README.md index 3f5e5d8..079e07a 100644 --- a/timer/README.md +++ b/timer/README.md @@ -6,7 +6,7 @@ of milliseconds and then [`return`](https://jsr.io/@observable/core/doc/~/Observ ## Build -Automated by [Deno](https://deno.land/) +Automated by [JSR](https://jsr.io/) ## Publishing