From 508309a24d48badec8fd529fe428984f37d5beaa Mon Sep 17 00:00:00 2001 From: Klaus Reimer Date: Thu, 16 Jan 2025 01:54:50 +0100 Subject: [PATCH] Improve types and get rid of @@observable nonsense --- README.md | 8 +++++- src/main/Observable.ts | 21 ++------------ src/main/ObservableLike.ts | 11 ++----- src/main/SubscribeArgs.ts | 18 ++++++++++++ src/main/index.ts | 3 +- src/test/Observable.test.ts | 2 ++ src/test/ObservableLike.test.ts | 48 +++++++++++++++++++++++++++++++ src/test/SharedObservable.test.ts | 2 ++ src/test/index.test.ts | 3 +- 9 files changed, 87 insertions(+), 29 deletions(-) create mode 100644 src/main/SubscribeArgs.ts create mode 100644 src/test/ObservableLike.test.ts diff --git a/README.md b/README.md index 42f9a43..ca3996e 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,13 @@ if (isUnsubscribable(something)) { ## RxJS compatibility -RxJS' Observable implementation is not standard-conform, especially because it uses a `pipe` method which is not part of the proposed standard. So in order to use Observable instances of this library in RxJS pipes you have to convert the observable with RxJS' `from` function: +RxJS' Observable implementation is not standard-conform, especially because it uses a `pipe` method which is not part of the proposed standard. RxJS unfortunately also does not use a polyfill for `Symbol.observable`, it just assumes that one is present or otherwise it falls back on using a non-standard `"@@observable"` string instead. To ensure compatibility between RxJS and any other observable implementation make sure to load a `Symbol.observable` polyfill like [symbol-observable] before importing RxJS: + +```typescript +import "symbol-observable"; +``` + +In order to use Observable instances of this library in RxJS pipes you have to convert the observable with RxJS' `from` function: ```typescript import { from } from "rxjs"; diff --git a/src/main/Observable.ts b/src/main/Observable.ts index 4f2f81f..ddfa50e 100644 --- a/src/main/Observable.ts +++ b/src/main/Observable.ts @@ -8,6 +8,7 @@ import "symbol-observable"; import { ObservableLike } from "./ObservableLike.js"; import { Observer } from "./Observer.js"; import { isSubscribable, Subscribable } from "./Subscribable.js"; +import type { SubscribeArgs } from "./SubscribeArgs.js"; import { SubscriberFunction } from "./SubscriberFunction.js"; import { Subscription } from "./Subscription.js"; import { SubscriptionImpl } from "./SubscriptionImpl.js"; @@ -41,18 +42,6 @@ export interface ObservableConstructor { from(observable: Subscribable | Iterable): ObservableLike; } -/** - * Subscriber arguments. - */ -export type SubscribeArgs = - | [ Observer ] - | [ (value: T) => void, ((error: Error) => void)?, (() => void)? ] - | [ - (((value: T) => void) | null | undefined), - (((error: any) => void) | null | undefined)?, - ((() => void) | null | undefined)? - ]; - /** * Observable implementation. */ @@ -92,14 +81,11 @@ export class Observable implements ObservableLike { return createObservableFrom(this, Observable, observable); } + /** @inheritDoc */ public [Symbol.observable](): this { return this; } - public "@@observable"(): this { - return this; - } - /** @inheritDoc */ public subscribe(observer: Observer): Subscription; @@ -167,8 +153,7 @@ export function createObservableFrom(thisConstructor: ObservableConstructor)["@@observable"] - ?? (observable as Observable)[Symbol.observable]; + const observableFactory = (observable as Observable)[Symbol.observable]; if (observableFactory instanceof Function) { const observable = observableFactory(); if (observable instanceof Object) { diff --git a/src/main/ObservableLike.ts b/src/main/ObservableLike.ts index 0a79bbf..5e97028 100644 --- a/src/main/ObservableLike.ts +++ b/src/main/ObservableLike.ts @@ -5,6 +5,7 @@ import { Observer } from "./Observer.js"; import { Subscribable } from "./Subscribable.js"; +import type { SubscribeArgs } from "./SubscribeArgs.js"; import { Subscription } from "./Subscription.js"; /** @@ -19,16 +20,10 @@ export interface ObservableLike extends Subscribable { Subscription; /** @inheritDoc */ - subscribe(...args: [ Observer ] | [ (value: T) => void, ((error: Error) => void)?, (() => void)? ]): - Subscription; + subscribe(...args: SubscribeArgs): Subscription; /** * Returns itself. */ - [Symbol.observable]?(): ObservableLike; - - /** - * Returns itself. This is used as a fallback for environments which don't support `Symbol.observable`. - */ - "@@observable"?(): ObservableLike; + [Symbol.observable](): ObservableLike; } diff --git a/src/main/SubscribeArgs.ts b/src/main/SubscribeArgs.ts new file mode 100644 index 0000000..d3c4bc1 --- /dev/null +++ b/src/main/SubscribeArgs.ts @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2018 Klaus Reimer + * See LICENSE.md for licensing information. + */ + +import type { Observer } from "./Observer.js"; + +/** + * Subscriber arguments. + */ +export type SubscribeArgs = + | [ Observer ] + | [ (value: T) => void, ((error: Error) => void)?, (() => void)? ] + | [ + (((value: T) => void) | null | undefined), + (((error: any) => void) | null | undefined)?, + ((() => void) | null | undefined)? + ]; diff --git a/src/main/index.ts b/src/main/index.ts index c7696c8..915cf6c 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -3,11 +3,12 @@ * See LICENSE.md for licensing information */ -export { Observable, type SubscribeArgs } from "./Observable.js"; +export { Observable } from "./Observable.js"; export { type ObservableLike } from "./ObservableLike.js"; export { type CompleteObserver, type ErrorObserver, type NextObserver, type Observer } from "./Observer.js"; export { SharedObservable } from "./SharedObservable.js"; export { isSubscribable, type Subscribable } from "./Subscribable.js"; +export { type SubscribeArgs } from "./SubscribeArgs.js"; export { type SubscriberFunction } from "./SubscriberFunction.js"; export { type Subscription } from "./Subscription.js"; export { type SubscriptionObserver } from "./SubscriptionObserver.js"; diff --git a/src/test/Observable.test.ts b/src/test/Observable.test.ts index 9ffbf0d..6a35bd5 100644 --- a/src/test/Observable.test.ts +++ b/src/test/Observable.test.ts @@ -3,6 +3,8 @@ * See LICENSE.md for licensing information. */ +import "symbol-observable"; + import { runTests as runObservableTests } from "es-observable-tests"; import { from, merge, Subject } from "rxjs"; import { describe, expect, it, vi } from "vitest"; diff --git a/src/test/ObservableLike.test.ts b/src/test/ObservableLike.test.ts new file mode 100644 index 0000000..21f2be9 --- /dev/null +++ b/src/test/ObservableLike.test.ts @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2025 Klaus Reimer + * See LICENSE.md for licensing information. + */ + +import "symbol-observable"; + +import { from, merge, Subject } from "rxjs"; +import { describe, expect, it } from "vitest"; + +import { Observable } from "../main/Observable.js"; +import type { ObservableLike } from "../main/ObservableLike.js"; +import type { SubscriptionObserver } from "../main/SubscriptionObserver.js"; + +describe("ObservableLike", () => { + it("can be converted into RxJS observable", () => { + let exposedObserver: SubscriptionObserver | undefined; + const observable: ObservableLike = new Observable(observer => { + exposedObserver = observer; + }); + const rxjsObservable = from(observable); + const values: number[] = []; + rxjsObservable.subscribe(value => values.push(value)); + if (exposedObserver == null) { + throw new Error("Observer not exposed"); + } + exposedObserver.next(1); + expect(values).toEqual([ 1 ]); + exposedObserver.next(2); + expect(values).toEqual([ 1, 2 ]); + }); + it("can be used in RxJS operators", () => { + let exposedObserver: SubscriptionObserver | undefined; + const observable: ObservableLike = new Observable(observer => { + exposedObserver = observer; + }); + const subject = new Subject(); + const mergedObservable = merge(subject, observable); + const values: number[] = []; + mergedObservable.subscribe(value => values.push(value)); + if (exposedObserver == null) { + throw new Error("Observer not exposed"); + } + exposedObserver.next(1); + subject.next(2); + expect(values).toEqual([ 1, 2 ]); + }); +}); diff --git a/src/test/SharedObservable.test.ts b/src/test/SharedObservable.test.ts index 3b84676..c875085 100644 --- a/src/test/SharedObservable.test.ts +++ b/src/test/SharedObservable.test.ts @@ -3,6 +3,8 @@ * See LICENSE.md for licensing information */ +import "symbol-observable"; + import { from } from "rxjs"; import { describe, expect, it, vi } from "vitest"; diff --git a/src/test/index.test.ts b/src/test/index.test.ts index b0f1573..d6ad832 100644 --- a/src/test/index.test.ts +++ b/src/test/index.test.ts @@ -6,11 +6,12 @@ import { describe, expect, it } from "vitest"; import * as exports from "../main/index.js"; -import { Observable, type SubscribeArgs } from "../main/Observable.js"; +import { Observable } from "../main/Observable.js"; import { type ObservableLike } from "../main/ObservableLike.js"; import { type CompleteObserver, type ErrorObserver, type NextObserver, type Observer } from "../main/Observer.js"; import { SharedObservable } from "../main/SharedObservable.js"; import { isSubscribable, type Subscribable } from "../main/Subscribable.js"; +import { type SubscribeArgs } from "../main/SubscribeArgs.js"; import { type SubscriberFunction } from "../main/SubscriberFunction.js"; import { type Subscription } from "../main/Subscription.js"; import { type SubscriptionObserver } from "../main/SubscriptionObserver.js";