Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 68 additions & 52 deletions AsyncClass.js
Original file line number Diff line number Diff line change
@@ -1,64 +1,80 @@
const gen_prototypes = function*(instance) {
let proto = instance
do {
yield proto
} while (proto = Object.getPrototypeOf(proto))
const genPrototypes = function * (instance) {
let proto = instance
do {
yield proto
} while (proto = Object.getPrototypeOf(proto))
}

const inheritance = Symbol("inheritance tracker")
const inheritance = Symbol('inheritanceTracker')

const async_constructor = Symbol("async constructor")
const async_super = Symbol("async super")
const constructor = Symbol('asyncConstructor')
const asyncSuper = Symbol('asyncSuper')

class AsyncClass {
constructor (...args) {
if (this[async_super] !== AsyncClass.prototype[async_super]) {
throw new ReferenceError("AsyncClass[[async super]] cannot be overriden")
}
return new Promise(async (resolve) => {
this[inheritance] = Object.getPrototypeOf(this)
const bound_constructor = this[inheritance].hasOwnProperty(async_constructor)
? this[async_constructor].bind(this)
: AsyncClass.prototype[async_constructor].bind(this)
delete this[async_constructor]
const override = await bound_constructor(...args)
if (typeof this[inheritance] !== "undefined" && this[inheritance] !== null && typeof this[inheritance][async_constructor] !== "undefined") {
throw new ReferenceError("this[[async super]]() must be called by this[[async constructor]]()")
}
delete this[async_super]
delete this[inheritance]
resolve(typeof override !== "undefined" ? override : this)
})
/**
* @type {import('./typedef.d.ts').AsyncClass}
*/
class Async {
constructor (...args) {
if (this[asyncSuper] !== Async.prototype[asyncSuper]) {
throw new ReferenceError('AsyncClass[[asyncSuper]] cannot be overriden')
}
return new Promise(async (resolve) => {
this[inheritance] = Object.getPrototypeOf(this)
const boundConstructor = this[inheritance].hasOwnProperty(constructor)
? this[constructor].bind(this)
: Async.prototype[constructor].bind(this)
delete this[constructor]
const override = await boundConstructor(...args)
if (typeof this[inheritance] !== 'undefined' && this[inheritance] !== null && typeof this[inheritance][constructor] !== 'undefined') {
throw new ReferenceError('this[[asyncSuper]]() must be called by this[[async constructor]]()')
}
delete this[asyncSuper]
delete this[inheritance]
resolve(typeof override !== 'undefined' ? override : this)
})
}

async [async_constructor](...args) {
await this[async_super](...args)
}
async [constructor] (...args) {
await this[asyncSuper](...args)
}

async [async_super](...args) {
this[inheritance] = Object.getPrototypeOf(this[inheritance])
if (typeof this[inheritance] !== "undefined" && this[inheritance] !== null && typeof this[inheritance][async_constructor] !== "undefined") {
if (this[inheritance].hasOwnProperty(async_constructor)) {
const override = await this[inheritance][async_constructor].bind(this)(...args)
if (typeof override !== "undefined") {
for (const proto of Array.from(gen_prototypes(this))) {
for (const key of Object.getOwnPropertyNames(proto)) {
delete this[key]
}
for (const key of Object.getOwnPropertySymbols(proto)) {
if (key !== async_constructor && key !== async_super && key !== inheritance) {
delete this[key]
}
}
}
Object.setPrototypeOf(this, Object.getPrototypeOf(override))
Object.assign(this, override)
}
} else {
await AsyncClass.prototype[async_constructor].bind(this)(...args)
async [asyncSuper] (...args) {
this[inheritance] = Object.getPrototypeOf(this[inheritance])
if (typeof this[inheritance] !== 'undefined' && this[inheritance] !== null && typeof this[inheritance][constructor] !== 'undefined') {
if (this[inheritance].hasOwnProperty(constructor)) {
const override = await this[inheritance][constructor].bind(this)(...args)
if (typeof override !== 'undefined') {
for (const proto of Array.from(genPrototypes(this))) {
for (const key of Object.getOwnPropertyNames(proto)) {
delete this[key]
}
for (const key of Object.getOwnPropertySymbols(proto)) {
if (key !== constructor && key !== asyncSuper && key !== inheritance) {
delete this[key]
}
}
}
Object.setPrototypeOf(this, Object.getPrototypeOf(override))
Object.assign(this, override)
}
} else {
await Async.prototype[constructor].bind(this)(...args)
}
}
}
}

/**
* @type {import('./typedef.d.ts').wrap}
*/
function wrap (_class) {
// @ts-ignore
return new Proxy(_class, {
construct: (Module, argumentsList) => {
const instance = new Module(...argumentsList)
return instance[constructor](...argumentsList)
}
})
}

module.exports = {AsyncClass, async_constructor, async_super}
module.exports = { Async, constructor, Super: asyncSuper, wrap }
59 changes: 36 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ This module provides a class and the symbols necessary to create classes whose c
The simplest async class looks like this:

```js
const {AsyncClass, async_constructor, async_super} = require("async-inheritance")
const { Async, constructor, Super } = require("async-inheritance")

class MyClass extends AsyncClass {
async [async_constructor]() {
await this[async_super]()
class MyClass extends Async {
async [constructor]() {
await this[Super]()
// async code
}
}
Expand All @@ -22,22 +22,35 @@ class MyClass extends AsyncClass {
})()
```

Classes that extend AsyncClass can then be extended further:
If you can't extend a function, simply wrap it:

```js
const {AsyncClass, async_constructor, async_super} = require("async-inheritance")
const { wrap } = require("async-inheritance")
const { EventEmitter } = require("events")

const WrappedClass = wrap(class extends EventEmitter {
async [constructor]() {
await asyncCode()
}
})
```

Classes that extend Async can then be extended further:

```js
const { Async, constructor, Super } = require("async-inheritance")
const timers = require("timers/promises")

class MyClass1 extends AsyncClass {
async [async_constructor](construction_delay) {
await this[async_super]()
class MyClass1 extends Async {
async [constructor](construction_delay) {
await this[Super]()
await timers.setTimeout(construction_delay)
}
}

class MyClass2 extends MyClass1 {
async [async_constructor]() {
await this[async_super](100)
async [constructor]() {
await this[Super](100)
console.log("Finished constructor")
}
}
Expand All @@ -47,15 +60,15 @@ class MyClass2 extends MyClass1 {
})()
```

If a child class overrides \[\[async_constructor]] it _must_ call \[\[async_super]], but the constructor can be omitted entirely with no issue:
If a child class overrides \[\[constructor]] it _must_ call \[\[Super]], but the constructor can be omitted entirely with no issue:

```js
const {AsyncClass, async_constructor, async_super} = require("async-inheritance")
const { Async, constructor, Super } = require("async-inheritance")
const timers = require("timers/promises")

class MyClass1 extends AsyncClass {
async [async_constructor](construction_delay) {
await this[async_super]()
class MyClass1 extends Async {
async [constructor](construction_delay) {
await this[Super]()
await timers.setTimeout(construction_delay)
}
do_something() {
Expand All @@ -71,8 +84,8 @@ class MyClass2 extends MyClass1 {
}

class MyClass3 extends MyClass2 {
async [async_constructor]() {
await this[async_super](100)
async [constructor]() {
await this[Super](100)
console.log("Finished constructor")
}
}
Expand All @@ -85,11 +98,11 @@ class MyClass3 extends MyClass2 {
Just like with normal constructors, the async constructor can return an object:

```js
const {AsyncClass, async_constructor, async_super} = require("async-inheritance")
const { Async, constructor, Super } = require("async-inheritance")

class MyClass extends AsyncClass {
async [async_constructor]() {
await this[async_super]()
class MyClass extends Async {
async [constructor]() {
await this[Super]()
return {my_property: 1}
}
}
Expand All @@ -103,7 +116,7 @@ class MyClass extends AsyncClass {
### Notes

* Overriding the normal constructor will break the async constructor's functionality
* Acessing `this` before calling `await this[async_super]()` may cause unexpected behavior (it does NOT throw an error the way that normal JavaScript class inheritance does)
* Acessing `this` before calling `await this[Super]()` may cause unexpected behavior (it does NOT throw an error the way that normal JavaScript class inheritance does)

### Compatibility

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "1.0.0",
"description": "Provide async constructor functionality to JavaScript classes",
"main": "AsyncClass.js",
"types": "typedef.d.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
Expand Down
9 changes: 9 additions & 0 deletions typedef.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface AsyncConstructable<T> extends T {
new (...args: ConstructorParameters<T>): Promise<T>
}

export interface AsyncClass {
new (...args: any[]): Promise<this>
}

export type wrap = <T>(_class: T) => AsyncConstructable<T>