2
\$\begingroup\$

Given a class where the methods return this you can chain them.

const e = new Example()
e.add(1).add(1).add(1).add(1).add(1)

However this breaks does not work if one of the methods is async and returns a promise.

I created an alternative method on compose the class Meow2 below that adds this kind of functionality.

const e = new Example()
e.flow(e => [
  e.add(1)
  e.asyncAdd(1)
  e.add(1)
  e.asyncAdd(1)
]).then(console.log)

I'm looking for any and all optimizations that can be made to this code.

Entire codebase:

export namespace Use {
    type ThenArg<T> = T extends Promise<infer U> ? U : T
    type RelatedThen<A, B> = A extends Promise<infer U> ? Promise<B> : B
    export type Func = (...args: any[]) => any
    export type Return<A extends Func, B> = RelatedThen<ReturnType<A>, B>
    export type CallbackValue<A extends Func> = ThenArg<ReturnType<A>>
}


export namespace Flow {
    export type Func = (args: any) => any
    export type Funcs = readonly Func[]
    type FuncPass<A, B> = (x: A) => B
    type Tail<T extends readonly FuncPass<any, any>[]> = FuncPass<T, void> extends ((h: any, ...r: infer R) => void) ? R : never;
    export type Check<T extends readonly FuncPass<any, any>[]> = { [K in keyof T]:
        K extends keyof Tail<T> ? (
            [T[K], Tail<T>[K]] extends [FuncPass<infer A, infer R>, FuncPass<infer S, any>] ? (
                [R] extends [S] ? T[K] : FuncPass<A, S>
            ) : never
        ) : T[K]
    }
    type FirstFnParam<Fns extends Funcs> = Parameters<Fns[0]>[0]
    export type Params<T extends Funcs> = FirstFnParam<T>
    type LengthOfTuple<T extends Funcs> = T extends { length: infer L } ? L : never;
    type DropFirstInTuple<T extends Funcs> = ((...args: T) => any) extends (arg: any, ...rest: infer U) => any ? U : T;
    type LastInTuple<T extends Funcs> = T[LengthOfTuple<DropFirstInTuple<T>>];
    type LastFnReturns<Fns extends Funcs> = ReturnType<LastInTuple<Fns>>
    export type Return<T extends Funcs> = LastFnReturns<T>
}

function nest<T>(value: T): () => T {
    return () => {
        return value
    }
}

function flow<T extends Flow.Funcs>(...funcs: Flow.Check<T>) {
    return (input: Flow.Params<T>) => {
        return funcs.reduce((acq: Flow.Func, fn: Flow.Func) => {
            return use(acq, (acqError, acqValue) => {
                if (acqError) throw acqError
                return use(fn, (fnError, fnValue) => {
                    if (fnError) throw fnError
                    return fnValue
                })(acqValue)
            })
        }, nest(input))(input) as Flow.Return<T>
    }
}

function isPromise(obj: any) {
    return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function'
}

function use<A extends Use.Func, G>(fn: A, callback: (e: Error | null, a: Use.CallbackValue<A>) => G) {
    return (...args: Parameters<A>): Use.Return<A, G> => {
        try {
            const v = fn(...args as any[])
            if (isPromise(v)) {
                return v.then((v: any) => callback(null, v)).catch((e: any) => callback(e, undefined as Use.CallbackValue<A>))
            }
            return callback(null, v) as Use.Return<A, G>
        } catch (e) {
            return callback(e, undefined as Use.CallbackValue<A>) as unknown as Use.Return<A, G>
        }
    }
}

type FlowClassNest<T extends (...args: any) => any> = (...a: Parameters<T>) => () => ReturnType<T>
type FlowCallbackArg<T> = { [K in keyof T]: T[K] extends ((...args: any) => any) ? FlowClassNest<T[K]> : never }
type FlowCallback<T> = (t: FlowCallbackArg<T>) => any[]

class Composition<T>{
    blacklist = [
        'constructor',
        '__defineGetter__',
        '__defineSetter__',
        'hasOwnProperty',
        '__lookupGetter__',
        '__lookupSetter__',
        'isPrototypeOf',
        'propertyIsEnumerable',
        'toString',
        'valueOf',
        '__proto__',
        'toLocaleString'
    ]

    constructor(private readonly parent: any) { }

    get methodNames() {
        let methods = new Set();
        let parent = this.parent
        while (parent = Reflect.getPrototypeOf(parent)) {
            let keys = Reflect.ownKeys(parent)
            keys.forEach((k) => methods.add(k));
        }
        return methods
    }
    get methodNamesArray() {
        return Array.from(this.methodNames)
    }
    get methodNamesArrayClean() {
        const arr1 = this.methodNamesArray
        const arr2 = this.blacklist
        return arr1.filter((x: string) => !arr2.includes(x));
    }
    get methods(): FlowCallbackArg<T> {
        const _methods = this.methodNamesArrayClean.map((key: string) => ({
            [key]: (...args) => {
                return () => {
                    return this.parent[key](...args)
                }
            }
        }))
        return Object.assign({}, ..._methods)
    }
    build(callback: FlowCallback<T>) {
        return flow(...callback(this.methods))(this.parent)
    }
}

Here's an example using above:

class Example {
    say: string
    setSay(value) {
        this.say = value
        return this
    }
    async setSayAsync(value) {
        this.say = value
        return this
    }
    logSay() {
        console.log(this.say)
        return this
    }
    compose(callback: FlowCallback<Meow2>) {
        return new Composition(this).build(callback)
    }
}

const m = new Example()
    .compose((m) => [
        m.setSay('A'),
        m.logSay(),
        m.setSayAsync('B'),
        m.logSay(),
        m.setSay('C'),
        m.logSay(),
    ])

m.then(console.log)
\$\endgroup\$
  • \$\begingroup\$ Are you sure that's what the code base looks like? What's the goal of it? Meow2 doesn't tell me much. \$\endgroup\$ – Mast Nov 25 '19 at 19:11
  • \$\begingroup\$ Meow2 Is an example case. \$\endgroup\$ – ThomasReggi Nov 25 '19 at 19:12
  • \$\begingroup\$ Which part of the code is not an example? \$\endgroup\$ – Mast Nov 25 '19 at 19:12
  • \$\begingroup\$ Everything above Meow2. \$\endgroup\$ – ThomasReggi Nov 25 '19 at 19:14
  • 1
    \$\begingroup\$ "However this breaks does not work if one of the methods is async and returns a promise. I created an alternative method on compose the class Meow2 below that adds this kind of functionality." -- But your example doesn't do the same thing, it does something similar. I'd suggest rewording this. This site is for working code and from a very broad point of view your code "doesn't work" since it's not doing that. I only mention that since it seems like this has a downvote and I am unsure why. (Personally I think it's fine.) \$\endgroup\$ – Captain Man Nov 25 '19 at 22:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.