Skip to content

Commit

Permalink
Support subclassing Observable with non-class constructor functions.
Browse files Browse the repository at this point in the history
Now that the zen-observable-ts package has the ability to export
Observable as a native class (#7615), we need to be careful when extending
Observable using classes (like ObservableQuery and Concast) that have been
compiled to ES5 constructor functions (rather than native classes),
because the generated _super.call(this, subscriber) code throws when
_super is a native class constructor (#7635).

Rather than attempting to change the way the TypeScript compiler
transforms super(subscriber) calls, this commit wraps Observable.call and
Observable.apply to work as expected, by using Reflect.construct to invoke
the superclass constructor correctly, when the Reflect API is available:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/construct

Another option would be to ship native class syntax with @apollo/client,
by changing the "target" in tsconfig.json from "es5" to "es2015" or later,
so that consumers of @apollo/client would be forced to compile native
class syntax however they see fit. That would be a more disruptive change,
in part because it would prevent subclassing Apollo Client-defined classes
using anything other than native class syntax and/or the Reflect.construct
API, which is the very same problem this commit is trying to fix for the
Observable class.
  • Loading branch information
benjamn committed Feb 2, 2021
1 parent a15a74e commit f38e02e
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 0 deletions.
27 changes: 27 additions & 0 deletions src/utilities/observables/Observable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,38 @@ import {
// proposal (https://github.com/zenparsing/es-observable)
import 'symbol-observable';

export type Subscriber<T> = ZenObservable.Subscriber<T>;
export type {
Observer,
ObservableSubscription,
};

Observable.call = function<T>(
this: typeof Observable,
obs: Observable<T>,
sub: ZenObservable.Subscriber<T>,
): Observable<T> {
return construct(this, obs, sub);
};

Observable.apply = function<T>(
this: typeof Observable,
obs: Observable<T>,
args: [ZenObservable.Subscriber<T>],
): Observable<T> {
return construct(this, obs, args[0]);
}

function construct<T>(
Super: typeof Observable,
self: Observable<T>,
subscriber: ZenObservable.Subscriber<T>,
): Observable<T> {
return typeof Reflect === 'object'
? Reflect.construct(Super, [subscriber], self.constructor)
: Function.prototype.call.call(Super, self, subscriber);
}

// Use global module augmentation to add RxJS interop functionality. By
// using this approach (instead of subclassing `Observable` and adding an
// ['@@observable']() method), we ensure the exported `Observable` retains all
Expand Down
69 changes: 69 additions & 0 deletions src/utilities/observables/__tests__/Observable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Observable, Subscriber } from '../Observable';

describe('Observable', () => {
describe('subclassing by non-class constructor functions', () => {
function check(constructor: new <T>(sub: Subscriber<T>) => Observable<T>) {
constructor.prototype = Object.create(Observable.prototype, {
constructor: {
value: constructor,
},
});

const subscriber: Subscriber<number> = observer => {
observer.next(123);
observer.complete();
};

const obs = new constructor(subscriber) as Observable<number>;

expect(typeof (obs as any).sub).toBe("function");
expect((obs as any).sub).toBe(subscriber);

expect(obs).toBeInstanceOf(Observable);
expect(obs).toBeInstanceOf(constructor);
expect(obs.constructor).toBe(constructor);

return new Promise((resolve, reject) => {
obs.subscribe({
next: resolve,
error: reject,
});
}).then(value => {
expect(value).toBe(123);
});
}

function newify(
constructor: <T>(sub: Subscriber<T>) => void,
): new <T>(sub: Subscriber<T>) => Observable<T> {
return constructor as any;
}

it('simulating super(sub) with Observable.call(this, sub)', () => {
function SubclassWithSuperCall<T>(sub: Subscriber<T>) {
const self = Observable.call(this, sub);
self.sub = sub;
return self;
}
return check(newify(SubclassWithSuperCall));
});

it('simulating super(sub) with Observable.apply(this, arguments)', () => {
function SubclassWithSuperApplyArgs<T>(_sub: Subscriber<T>) {
const self = Observable.apply(this, arguments);
self.sub = _sub;
return self;
}
return check(newify(SubclassWithSuperApplyArgs));
});

it('simulating super(sub) with Observable.apply(this, [sub])', () => {
function SubclassWithSuperApplyArray<T>(...args: [Subscriber<T>]) {
const self = Observable.apply(this, args);
self.sub = args[0];
return self;
}
return check(newify(SubclassWithSuperApplyArray));
});
});
});

0 comments on commit f38e02e

Please sign in to comment.