Skip to content

Instantly share code, notes, and snippets.

@RomkeVdMeulen
Last active May 10, 2018 17:48
Show Gist options
  • Save RomkeVdMeulen/fb5fbbfd2f8154c6cb4590f058cdb728 to your computer and use it in GitHub Desktop.
Save RomkeVdMeulen/fb5fbbfd2f8154c6cb4590f058cdb728 to your computer and use it in GitHub Desktop.
@init / @waitOnInit TypeScript method decorators
const INIT_METHODS = new Map<any, string[]>();
type InitMethodDescriptor = TypedPropertyDescriptor<() => void>
| TypedPropertyDescriptor<() => Promise<void>>;
export function init(target: any, key: string, _descriptor: InitMethodDescriptor) {
if (!INIT_METHODS.has(target)) {
INIT_METHODS.set(target, []);
}
INIT_METHODS.get(target)!.push(key);
}
const INIT_PROMISE_SYMBOL = Symbol.for("init_promise");
export function waitOnInit(target: any, _key: string, descriptor: PropertyDescriptor) {
const method = descriptor.value!;
descriptor.value = function(...args: any[]) {
if (!Object.getOwnPropertySymbols(this).includes(INIT_PROMISE_SYMBOL)) {
if (!INIT_METHODS.has(target)) {
this[INIT_PROMISE_SYMBOL] = Promise.resolve();
} else {
const promises = INIT_METHODS.get(target)!.map(methodname => {
return Promise.resolve(this[methodname]());
});
this[INIT_PROMISE_SYMBOL] = Promise.all(promises);
}
}
return this[INIT_PROMISE_SYMBOL].then(() => method.apply(this, args));
};
return descriptor;
}
describe("@init / @waitOnInit", () => {
it("registers methods to run during init, and methods that must wait on init", async () => {
class WithInit {
dataLoads = 0;
initOperationCalled = 0;
@init
protected async loadData() {
this.dataLoads++;
}
@init
protected initOperation() {
this.initOperationCalled++;
}
withoutInit() {}
@waitOnInit
withInit() {}
}
const instance = new WithInit();
expect(instance.dataLoads).to.equal(0);
expect(instance.initOperationCalled).to.equal(0);
expect(instance.withoutInit()).not.to.be.a("Promise");
expect(instance.dataLoads).to.equal(0);
expect(instance.initOperationCalled).to.equal(0);
const withInit = instance.withInit();
expect(withInit).to.be.a("Promise");
await withInit;
expect(instance.dataLoads).to.equal(1);
expect(instance.initOperationCalled).to.equal(1);
await instance.withInit();
expect(instance.dataLoads).to.equal(1);
expect(instance.initOperationCalled).to.equal(1);
});
it("throws the first encountered error during init for all methods waiting on init", () => {
class InitError {
@init
protected async withError() {
throw new Error("init error");
}
@init
protected async withLaterError() {
await new Promise(res => setTimeout(res, 10));
throw new Error("later init error");
}
@waitOnInit
afterInit() {
return true;
}
@waitOnInit
throwsOwnError() {
throw new Error("post-init error");
}
}
const instance = new InitError();
expect(instance.afterInit()).to.be.a("Promise")
.that.is.eventually.rejectedWith(Error, "init error");
expect(instance.throwsOwnError()).to.be.a("Promise")
.that.is.eventually.rejectedWith(Error, "init error");
});
it("can handle classes that use only @init or only @waitOnInit", () => {
class NoInit {
@waitOnInit
noInit() {
return true;
}
}
expect((new NoInit()).noInit()).to.be.a("Promise")
.that.eventually.is.true;
class NoWait {
initCalled = false;
@init
someInit() {
this.initCalled = true;
}
noWait() {}
}
const noWait = new NoWait();
expect(noWait.noWait()).not.to.be.a("Promise");
expect(noWait.initCalled).to.be.false;
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment