Typescript Promise API 不知道為什麼,只有提供 callback style 的 API ,對於用過 Scala Promise 的用戶來說。使用上很難用,可讀性又很糟糕

return new Promise<number>((resolve, reject) => {
    try {
        //do something
        resolve(1)
    } catch (error) {
        reject(error)
    }
});

這樣子的寫法,把主要的邏輯包在 callback 內,可讀性不高,如果 promise 內又有 promise 就更難讀了。所以隨手寫了個 wrapper ,把包在 callback 內的 resolve 跟 reject 釋放出來,可以直接呼叫

/**
 * Wrapper that expose the `resolve` and `reject` functions to enable scala style promise API.
 *
 * <code>
 *     const p = promise<number>()
 *     p.resolve(3)
 *     await p.await()
 * </code>
 */
class PromiseWrapper<T> {
    private readonly _promise: Promise<T>
    private _resolve
    private _reject
    private _resolved = false

    constructor() {
        this._promise = new Promise<T>(((resolve, reject) => {
            this._resolve = resolve
            this._reject = reject
        }))
    }
    private resolved() {
        if (this._resolved) {
            throw new Error("Promise is resolved already.")
        }
        this._resolved = true;
    }
    resolve<T>(value: T) {
        this.resolved()
        this._resolve(value)
    }

    reject(error: Error) {
        this.resolved()
        this._reject(error)
    }

    await(): Promise<T> {
        return this._promise
    }
}

export function promise<T>(): PromiseWrapper<T> {
    return new PromiseWrapper()
}

這下子程式可以改成這樣寫


const p = promise<number>()
try {
    // do something
    p.resolve(1)
} catch(error) {
    p.reject(error)
}

看起來效益不高,但是這寫法真正的好處是,可以把 promise 當成一個變數,傳到其他的函式內,在其它的地方呼叫 promise.resolve(…)

這種寫法,在寫非同步程式的測式時很好用,在底下的例子中, Engine 是個 PubSub ,會非同步執行 workflow 內容。

因為是在背景執行又沒有吐出資料,那麼我們要怎麼測試這個 Engine 是不是每一步都有執行到,又要怎麼樣把每一步的結果傳出來到測試當中呢?

這時 promise(...) 就很好用了,先把 promise 開好,再傳到非同步的函式內去呼叫 resolve(...) ,這樣子就可以把中間的狀態送到外層去,而且在測式內也不用寫很醜的 sleep(5000) 。

sleep(5000)一開始付出的成本還好,等到專案寫了一年後,有上百個測式都在 sleep(5000) ,然後偶爾因為 CI 的機器太慢,等的不夠久造成程式失敗,重跑又要十幾分鐘,你就會知道為什麼要用 promise API 了。

describe("workflow", () => {
    const engine = new Engine(pub)
    it("can step through steps", async () => {
        const name = randomString(10)
        const result = promise<any>()
        const workflow = WorkflowBuilder.newBuilder(name)
            .from({
                name: "step-1",
                run(context: Context, event: Event) {
                    return 2
                }
            })
            .to({
                name: "step-2",
                run(context: Context, event: Event) {
                    result.resolve(context)
                    return
                }
            })
            .build()
        engine.register(workflow)
        engine.trigger(workflow, {"msg": "hello"})

        const ret = await result.await()
        expect(ret).toStrictEqual({...})
    })
})