-
-
Save CMCDragonkai/1dbf5069d9efc11585c27cc774271584 to your computer and use it in GitHub Desktop.
/** | |
* 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 | |
} |
/** | |
* 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(); | |
} |
/** | |
* 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(); | |
} |
/** | |
* 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(); | |
} |
/** | |
* 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 | |
} |
Other ways are listed here: https://2ality.com/2019/11/creating-class-instances.html
There is a typescript unfriendly way with using promises in the constructor itself. You have to tell TS to ignore the fact that the constructor is returning a promise.
Furthermore extending the class becomes complicated too.
@matrixai/workers
uses Async Create & Destroy - there's no use for a non-running WorkerManager@matrixai/db
uses Async Create & Destroy & Start & Stop - you may wish to open and close the DB, but also separately destroy the DB
Because we don't use async-mutex
in start
, stop
or otherwise. This means these methods including destroy
are "non-blocking" in concurrent scenarios.
I believe this is better than mandating the use of locks here, since it is possible for the programmer to add in locks externally on top of the system later to manage concurrency. Instead starting out non-blocking is lighterweight and doesn't use any extra dependencies here.
Attempting to use decorators to generalize this. Here's an example for start & stop.
function StartStop<
T extends {
new (...args: any[]): {
start(): Promise<void>;
stop(): Promise<void>;
};
}
>(constructor: T) {
return class extends constructor {
protected _running: boolean = false;
get running(): boolean {
return this._running;
}
public async start(): Promise<void> {
try {
if (this._running) {
return;
}
this._running = true;
await super.start();
} catch (e) {
this._running = false;
throw e;
}
}
public async stop() {
try {
if (!this._running) {
return;
}
this._running = false;
await super.stop();
} catch (e) {
this._running = true;
throw e;
}
}
};
}
function Running(e: Error = new Error()) {
return function runnable(
target: Object,
key: string | symbol,
descriptor: PropertyDescriptor
) {
const f = descriptor.value;
if (typeof f !== 'function') {
throw new Error;
}
descriptor.value = function (...args) {
if (!this._running) {
throw e;
}
return f.apply(this, args);
}
return descriptor;
}
}
@StartStop
class X {
protected y: Y;
public constructor({ y = new Y }: { y?: Y } = {}) {
this.y = y;
}
public async start (): Promise<void> {
await this.y.start();
console.log('X started');
}
public async stop(): Promise<void> {
console.log('X stopped');
}
@Running()
public async doSomething(): Promise<void> {
console.log('X did something');
}
}
@StartStop
class Y {
public constructor() {
}
public async start () {
console.log('Y started');
}
public async stop () {
console.log('Y stopped');
}
}
async function main () {
const o = new X();
console.log(o);
// @ts-ignore
console.log('X running', o.running);
// @ts-ignore
await o.start();
// @ts-ignore
console.log('X running', o.running);
await o.doSomething();
// @ts-ignore
await o.stop();
// @ts-ignore
console.log('X running', o.running);
}
main();
Notice how the o
doesn't get the type of the decoration. There are apparently 2 methods to solve this.
- Change the way we construct
X
, toconst X = StartStop(class { ... });
. - Force type cast
The relevant issue for this problem is: microsoft/TypeScript#4881. It's still under discussion how to solve this.
Found a solution, now it works:
interface StartStop {
get running(): boolean;
start(...args: Array<any>): Promise<void>;
stop(...args: Array<any>): Promise<void>;
}
function StartStop<
T extends {
new (...args: any[]): {
start?(...args: Array<any>): Promise<void>;
stop?(...args: Array<any>): Promise<void>;
};
}
>(constructor: T) {
return class extends constructor {
protected _running: boolean = false;
get running(): boolean {
return this._running;
}
public async start(...args: Array<any>): Promise<void> {
try {
if (this._running) {
return;
}
this._running = true;
if (typeof super['start'] === 'function') {
await super.start(...args);
}
} catch (e) {
this._running = false;
throw e;
}
}
public async stop(...args: Array<any>) {
try {
if (!this._running) {
return;
}
this._running = false;
if (typeof super['stop'] === 'function') {
await super.stop(...args);
}
} catch (e) {
this._running = true;
throw e;
}
}
};
}
function Running(e: Error = new Error()) {
return function runnable(
target: Object,
key: string | symbol,
descriptor: PropertyDescriptor
) {
const f = descriptor.value;
if (typeof f !== 'function') {
throw new Error;
}
descriptor.value = function (...args) {
if (!this._running) {
throw e;
}
return f.apply(this, args);
}
return descriptor;
}
}
interface X extends StartStop {};
@StartStop
class X {
protected y: Y;
public constructor({ y = new Y }: { y?: Y } = {}) {
this.y = y;
}
public async start(p: number): Promise<void> {
await this.y.start(1);
console.log('X started', p);
}
public async stop(): Promise<void> {
await this.y.stop();
console.log('X stopped');
}
@Running()
public async doSomething(): Promise<void> {
console.log('X did something');
await this.y.doSomething();
}
}
interface Y extends StartStop {};
@StartStop
class Y {
public constructor() {
}
public async start (p: number) {
console.log('Y started', p);
}
public async stop () {
console.log('Y stopped');
}
@Running(new Error('CustomError'))
public async doSomething(): Promise<void> {
console.log('Y did something');
}
}
async function main () {
const o = new X();
console.log(o);
console.log('X running', o.running);
await o.start(1);
console.log('X running', o.running);
await o.doSomething();
await o.stop();
console.log('X running', o.running);
}
main();
It also works for create & destroy.
interface CreateDestroy {
get destroyed(): boolean;
destroy(...args: Array<any>): Promise<void>;
}
function CreateDestroy<
T extends {
new (...args: any[]): {
destroy?(...args: Array<any>): Promise<void>;
};
}
>(constructor: T) {
return class CreateDestroy extends constructor {
protected _destroyed: boolean = false;
get destroyed(): boolean {
return this._destroyed;
}
public async destroy(...args: Array<any>): Promise<void> {
try {
if (this._destroyed) {
return;
}
this._destroyed = true;
if (typeof super['destroy'] === 'function') {
await super.destroy(...args);
}
} catch (e) {
this._destroyed = false;
throw e;
}
}
}
}
function Ready(e: Error = new Error()) {
return (
target: Object,
key: string,
descriptor: PropertyDescriptor
) => {
const f = descriptor.value;
if (typeof f !== 'function') {
throw new TypeError(`${key} is not a function`);
}
descriptor.value = function (...args) {
if (this._destroyed) {
throw e;
}
return f.apply(this, args);
}
return descriptor;
};
}
interface X extends CreateDestroy {};
@CreateDestroy
class X {
protected y: Y;
public static async createX(
{
y
}: {
y?: Y
} = {}
) {
y = y ?? await Y.createY();
return new X({ y });
}
public constructor ({ y }: { y: Y }) {
this.y = y;
}
public async destroy(): Promise<void> {
await this.y.destroy();
console.log('X destroyed');
}
@Ready()
public async doSomething() {
await this.y.doSomething();
console.log('X did something');
}
}
interface Y extends CreateDestroy {};
@CreateDestroy
class Y {
public static async createY() {
return new Y;
}
public constructor () {
}
public async destroy(): Promise<void> {
console.log('Y destroyed');
}
@Ready(new Error('CustomError'))
public async doSomething(): Promise<void> {
console.log('Y did something');
}
@Ready()
public async giveSomething(): Promise<number> {
return 1;
}
}
async function main() {
const x = await X.createX();
console.log(x.destroyed);
await x.doSomething();
await x.destroy();
console.log(x.destroyed);
}
main();
The only problem is that the constructor has to be public for now. It's not a big deal to leave it as is.
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
Note that static methods are subject to the subtyping polymorphic constraint. Therefore we cannot use the same
create
method name in subclasses as we may need to vary our asynchronous initialisation parameters and return type. We can deal with this by suffixing ourcreateX
whereX
is the name of the class being created.Ideally in the future JavasScript has some sort of asynchronous constructor provided as constructors can currently be overloaded and are not subject to subtyping requirement.
It is also possible to put the asynchronous creation method outside of the class entirely. But that makes it more difficult to bundle things together syntactically.