Skip to content

Commit

Permalink
Support subclassing Observable with non-class constructor functions (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
benjamn authored Feb 2, 2021
1 parent 86b7444 commit ae559f5
Show file tree
Hide file tree
Showing 3 changed files with 102 additions and 2 deletions.
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ import Observable = require("zen-observable");
export { Observable };
export type Observer<T> = ZenObservable.Observer<T>;
export type Subscription = ZenObservable.Subscription;
export type Subscriber<T> = ZenObservable.Subscriber<T>;
30 changes: 29 additions & 1 deletion module.js
Original file line number Diff line number Diff line change
@@ -1 +1,29 @@
export { Observable } from "zen-observable/esm.js";
import { Observable } from "zen-observable/esm.js";

// When a non-native class constructor function attempts to subclass
// Observable, compilers like Babel and TypeScript may compile the
// super(subscriber) expression to Observable.call(this, subscriber) or
// Observable.apply(this, arguments). Calling a native class constructor
// in this way is forbidden by the ECMAScript specification, but we can
// preserve backwards compatibility by overriding the Observable.call and
// Observable.apply methods with an implementation that calls
// Reflect.construct instead, when available.

Observable.call = function (instance, subscriber) {
// Since Observable.call is a static method, 'this' will typically be the
// Observable constructor function, though it could be a subclass of the
// Observable constructor, which is why we don't just hard-code it here.
return construct(this, instance, subscriber);
};

Observable.apply = function (instance, args) {
return construct(this, instance, args[0]);
};

function construct(Super, instance, subscriber) {
return typeof Reflect === 'object'
? Reflect.construct(Super, [subscriber], instance.constructor)
: Function.prototype.call.call(Super, instance, subscriber) || instance;
}

export { Observable }
73 changes: 72 additions & 1 deletion tests/Observable.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,80 @@
const { expect } = require("chai");
const { expect } = require("chai") as typeof import("chai");

import type {
Observable,
Subscriber,
} from "..";

export default function (Observable: typeof import("..").Observable) {
describe("Observable", () => {
it("Should be a constructor function", () => {
expect(typeof Observable).to.equal("function");
});

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).to.equal("function");
expect((obs as any).sub).to.equal(subscriber);

expect(obs).to.be.instanceOf(Observable);
expect(obs).to.be.instanceOf(constructor);
expect(obs.constructor).to.equal(constructor);

return new Promise((resolve, reject) => {
obs.subscribe({
next: resolve,
error: reject,
});
}).then(value => {
expect(value).to.equal(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) || this;
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) || this;
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) || this;
self.sub = args[0];
return self;
}
return check(newify(SubclassWithSuperApplyArray));
});
});
});
}

0 comments on commit ae559f5

Please sign in to comment.