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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
239 changes: 236 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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<In, Out>(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<In, Out>(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<number> = [];

// 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);
}
```
65 changes: 65 additions & 0 deletions all/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# @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.

## Build

Automated by [JSR](https://jsr.io/)

## 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
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)
2 changes: 1 addition & 1 deletion web/deno.json → all/deno.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@xan/observable-web",
"name": "@observable/all",
"version": "0.1.0",
"license": "MIT",
"exports": "./mod.ts"
Expand Down
21 changes: 10 additions & 11 deletions common/all.test.ts → all/mod.test.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
Loading