Last active
September 30, 2021 07:41
-
-
Save CMCDragonkai/1dbf5069d9efc11585c27cc774271584 to your computer and use it in GitHub Desktop.
Asynchronous Initialisation and Deinitialisation for JavaScript Classes #typescript #javascript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Use this when object lifetime matches object "readiness" | |
*/ | |
class X { | |
protected _destroyed: boolean = false; | |
public static async createX(): Promise<X> { | |
return new X; | |
} | |
protected constructor() { | |
} | |
get destroyed(): boolean { | |
return this._destroyed; | |
} | |
public async destroy(): Promise<void> { | |
try { | |
if (this._destroyed) { | |
return; | |
} | |
this._destroyed = true; | |
} catch (e) { | |
this._destroyed = false; | |
throw e; | |
} | |
} | |
public async doSomething(): Promise<void> { | |
if (this._destroyed) { | |
throw new Error('X is destroyed'); | |
} | |
console.log('X did something'); | |
} | |
} | |
class Y extends X { | |
public static async createY(): Promise<Y> { | |
return new Y; | |
} | |
protected constructor() { | |
super(); | |
} | |
public async destroy(): Promise<void> { | |
await super.destroy(); | |
} | |
} | |
async function main() { | |
const x = await X.createX(); | |
await x.destroy(); | |
// once destroyed you must never use it | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* X is a create & destroy pattern | |
* This uses composition pattern, not inheritance | |
*/ | |
class X { | |
/** | |
* Public to indicate non-encapsulation of Y | |
*/ | |
public y: Y; | |
/** | |
* Protected to indicate Z it is encapsulated and managed by X | |
*/ | |
protected z: Z; | |
protected _destroyed: boolean = false; | |
/** | |
* Y is a dependency but not encapsulated | |
* Z is an optional and overridable encapsulated dependency | |
* Default parameters are always put in the createX, not in the constructor | |
*/ | |
public static async createX({ | |
y, | |
z | |
}: { | |
y: Y, | |
z?: Z | |
}): Promise<X> { | |
z = z ?? await Z.createZ(); | |
const x = new X({ y, z }); | |
return x; | |
} | |
/** | |
* It is assumed that Y and Z are ready to be used | |
*/ | |
protected constructor({ y, z }: { y: Y, z: Z }) { | |
this.y = y; | |
this.z = z; | |
} | |
get destroyed() { | |
return this._destroyed; | |
} | |
public async destroy(): Promise<void> { | |
try { | |
if (this._destroyed) { | |
return; | |
} | |
this._destroyed = true; | |
// Z is managed by X therefore it should destroy it | |
await this.z.destroy(); | |
} catch (e) { | |
this._destroyed = false; | |
throw e; | |
} | |
} | |
public async useYZ(): Promise<void> { | |
if (this._destroyed) { | |
throw new Error('Already destroyed!'); | |
} | |
await this.y.doSomething(); | |
await this.z.doSomething(); | |
} | |
} | |
class Y { | |
protected _destroyed: boolean = false; | |
public static async createY() { | |
const y = new Y; | |
return y; | |
} | |
protected constructor() { | |
} | |
public async destroy(): Promise<void> { | |
try { | |
if (this._destroyed) { | |
return; | |
} | |
this._destroyed = true; | |
} catch (e) { | |
this._destroyed = false; | |
throw e; | |
} | |
} | |
public async doSomething(): Promise<void> { | |
if (this._destroyed) { | |
throw new Error('Y is destroyed'); | |
} | |
console.log('Something from Y'); | |
} | |
} | |
class Z { | |
protected _destroyed: boolean = false; | |
public static async createZ(): Promise<Z> { | |
const z = new Z; | |
return z; | |
} | |
protected constructor() { | |
} | |
public async destroy(): Promise<void> { | |
try { | |
if (this._destroyed) { | |
return; | |
} | |
this._destroyed = true; | |
} catch (e) { | |
this._destroyed = false; | |
throw e; | |
} | |
} | |
public async doSomething(): Promise<void> { | |
if (this._destroyed) { | |
throw new Error('Z is destroyed'); | |
} | |
console.log('Something from Z'); | |
} | |
} | |
async function main() { | |
const y = await Y.createY(); | |
const x = await X.createX({ | |
y | |
}); | |
await x.useYZ(); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Use this when object lifetime outlives object "readiness" | |
* This means there is some use of the object when the object isn't running | |
*/ | |
class X { | |
protected _running: boolean = false; | |
public constructor() { | |
} | |
get running(): boolean { | |
return this._running; | |
} | |
public async start(): Promise<void> { | |
try { | |
if (this._running) { | |
return; | |
} | |
this._running = true; | |
} catch (e) { | |
this._running = false; | |
throw e; | |
} | |
} | |
public async stop() { | |
try { | |
if (!this._running) { | |
return; | |
} | |
this._running = false; | |
} catch (e) { | |
this._running = true; | |
throw e; | |
} | |
} | |
public async doSomething() { | |
if (!this._running) { | |
throw new Error('X is not running'); | |
} | |
console.log('X did something'); | |
} | |
} | |
class Y extends X { | |
public constructor() { | |
super(); | |
} | |
public async start() { | |
await super.start(); | |
} | |
public async stop() { | |
await super.stop(); | |
} | |
} | |
async function main() { | |
const x = new X; | |
await x.start(); | |
await x.stop(); | |
await x.start(); | |
await x.stop(); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* X is a start & stop pattern | |
* This uses composition pattern, not inheritance | |
*/ | |
class X { | |
/** | |
* Public to indicate non-encapsulation of Y | |
*/ | |
public y: Y; | |
/** | |
* Protected to indicate Z it is encapsulated and managed by X | |
*/ | |
protected z: Z; | |
protected _running: boolean = false; | |
/** | |
* Y is a dependency but not encapsulated | |
* Z is an optional and overridable encapsulated dependency | |
*/ | |
public constructor({ | |
y, | |
z = new Z | |
}: { | |
y: Y, | |
z?: Z | |
}) { | |
this.y = y; | |
this.z = z; | |
} | |
get running(): boolean { | |
return this._running; | |
} | |
/** | |
* It is assumed that Y is already started | |
* This will start Z because Z is encapsulated | |
*/ | |
public async start(): Promise<void> { | |
try { | |
if (this._running) { | |
return; | |
} | |
this._running = true; | |
await this.z.start(); | |
} catch (e) { | |
this._running = false; | |
throw e; | |
} | |
} | |
public async stop() { | |
try { | |
if (!this._running) { | |
return; | |
} | |
this._running = false; | |
await this.z.stop(); | |
} catch (e) { | |
this._running = true; | |
throw e; | |
} | |
} | |
public async useYZ() { | |
if (!this._running) { | |
throw new Error('X is not running'); | |
} | |
await this.y.doSomething(); | |
await this.z.doSomething(); | |
} | |
} | |
class Y { | |
protected _running: boolean = false; | |
public constructor() { | |
} | |
get running(): boolean { | |
return this._running; | |
} | |
public async start(): Promise<void> { | |
try { | |
if (this._running) { | |
return; | |
} | |
this._running = true; | |
} catch (e) { | |
this._running = false; | |
throw e; | |
} | |
} | |
public async stop() { | |
try { | |
if (!this._running) { | |
return; | |
} | |
this._running = false; | |
} catch (e) { | |
this._running = true; | |
throw e; | |
} | |
} | |
public async doSomething() { | |
if (!this._running) { | |
throw new Error('Y is not running'); | |
} | |
console.log('Y did something'); | |
} | |
} | |
class Z { | |
protected _running: boolean = false; | |
public constructor() { | |
} | |
get running(): boolean { | |
return this._running; | |
} | |
public async start(): Promise<void> { | |
try { | |
if (this._running) { | |
return; | |
} | |
this._running = true; | |
} catch (e) { | |
this._running = false; | |
throw e; | |
} | |
} | |
public async stop() { | |
try { | |
if (!this._running) { | |
return; | |
} | |
this._running = false; | |
} catch (e) { | |
this._running = true; | |
throw e; | |
} | |
} | |
public async doSomething() { | |
if (!this._running) { | |
throw new Error('Z is not running'); | |
} | |
console.log('Z did something'); | |
} | |
} | |
async function main() { | |
const y = new Y(); | |
const x = new X({ | |
y: y | |
}); | |
// you can start y and x out of order | |
await y.start(); | |
await x.start(); | |
await x.useYZ(); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Use this when you need to start and stop and explicitly destroy the object | |
* Once destroyed, you can create it again | |
* If you stop it, you can start it again | |
* Remember you can only destroy after stopping | |
*/ | |
class X { | |
protected _running: boolean = false; | |
protected _destroyed: boolean = false; | |
public static async createX(): Promise<X> { | |
const x = new X; | |
await x.start(); | |
return x; | |
} | |
protected constructor() { | |
} | |
get running() { | |
return this._running; | |
} | |
get destroyed() { | |
return this._destroyed; | |
} | |
public async start(): Promise<void> { | |
try { | |
if (this._running) { | |
return; | |
} | |
if (this._destroyed) { | |
throw new Error('X is destroyed'); | |
} | |
this._running = true; | |
} catch (e) { | |
this._running = false; | |
throw e; | |
} | |
} | |
public async stop(): Promise<void> { | |
try { | |
if (!this._running) { | |
return; | |
} | |
if (this._destroyed) { | |
throw new Error('X is destroyed'); | |
} | |
this._running = false; | |
} catch (e) { | |
this._running = true; | |
throw e; | |
} | |
} | |
public async destroy(): Promise<void> { | |
try { | |
if (this._destroyed) { | |
return; | |
} | |
if (this._running) { | |
throw new Error('X is running'); | |
} | |
this._destroyed = true; | |
} catch (e) { | |
this._destroyed = false; | |
throw e; | |
} | |
} | |
public async doSomething(): Promise<void> { | |
if (!this._running) { | |
throw new Error('X is not running'); | |
} | |
console.log('X did something'); | |
} | |
} | |
async function main() { | |
const x = await X.createX(); // x is started on creation | |
await x.doSomething(); | |
await x.start(); // this is a noop | |
await x.doSomething(); | |
await x.stop(); // stops x | |
await x.start(); // restarts x | |
await x.doSomething(); | |
await x.stop(); // stops x | |
await x.destroy(); // destroy x only after stopping | |
} |
So now the method decorator is always Ready()
. One needs to pass a custom exception to be thrown when it isn't ready.
Finally for async create destroy and start stop.
This is now generalised to https://github.com/MatrixAI/js-async-init
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
It also works for create & destroy.
The only problem is that the constructor has to be public for now. It's not a big deal to leave it as is.