Skip to content

✨ Feat: Promise #5

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
2 changes: 1 addition & 1 deletion assignment/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/curring-partial.ts"></script>
<script type="module" src="/src/promise.ts"></script>
</body>
</html>
63 changes: 63 additions & 0 deletions assignment/src/promise.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import MyPromise from "./promise"

type Callback<T> = (resolve: (value: T) => void, reject: (reason?: any) => void) => void

function testPromise<T>(callback: Callback<T>) {
return new MyPromise<T>((resolve, reject) => {
setTimeout(() => {
callback(resolve, reject)
}, 300)
})
}

describe("프로미스 테스트", () => {
test("then 메서드 테스트", () => {
return testPromise<number>((resolve) => resolve(1)).then((value) => expect(value).toEqual(1))
})

test("then 메서드 체이닝", () => {
return testPromise<number>((resolve) => resolve(1))
.then((value) => value + 1)
.then((value) => value + 1)
.then((value) => expect(value).toEqual(3))
})

test("catch 메서드 테스트 베이직", () => {
return testPromise<number>((_, reject) => reject("에러 발생")).catch((error) => {
expect(error).toBe("에러 발생")
})
})

test("catch 메서드 테스트 스탠다드", () => {
return testPromise<number>((resolve) => resolve(1))
.then((value) => value + 1)
.then((value) => {
if (value < 5) {
throw new Error("value가 5보다 작아요")
}

return value
})
.catch((error) => {
expect(error).toBeInstanceOf(Error)
})
})

test("finally 메서드 테스트", () => {
const finallyMockFn = vi.fn(() => "비동기 성공, 실패 여부와 상관 없이 실행")

return testPromise<number>((resolve) => resolve(1))
.then((value) => value + 1)
.then((value) => {
expect(value).toEqual(2)
throw new Error("에러 발생")
})
.catch((error) => {
expect(error).toBeInstanceOf(Error)
})
.finally(finallyMockFn)
.then(() => {
expect(finallyMockFn).toHaveBeenCalledTimes(1)
})
})
})
105 changes: 105 additions & 0 deletions assignment/src/promise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
type Executor<T> = (resolve: Resolve<T>, reject: Reject) => void
type Resolve<T extends unknown> = (value: T) => void | T
type Reject = (reason?: any) => void

export default class MyPromise<T> {
private promiseState: "pending" | "fulfilled" | "rejected" = "pending"
private promiseResult: T | null | undefined = null
private resolveQueue: any[] = []
private rejectQueue: any[] = []

constructor(executor: Executor<T>) {
const bindingResolve = this._resolve.bind(this)
const bindingReject = this._reject.bind(this)

try {
executor(bindingResolve, bindingReject)
} catch (error) {
bindingReject(error)
}
}

static resolve<T>(value: T) {
return new MyPromise<T>((resolve) => resolve(value))
}

static reject<T>(value: T) {
return new MyPromise<T>((_, reject) => reject(value))
}

private _resolve(value: T) {
if (this.promiseState !== "pending") {
return
}

this.promiseState = "fulfilled"
this.promiseResult = value

this.resolveQueue.forEach((fn) => fn(this.promiseResult))
this.resolveQueue = []
}

private _reject(reason?: any) {
if (this.promiseState !== "pending") {
return
}

this.promiseState = "rejected"
this.promiseResult = reason
this.rejectQueue.forEach((fn) => fn(this.promiseResult))
this.rejectQueue = []
}

catch(onRejected: Reject) {
return this.then(undefined, onRejected)
}

then(onFulfilled: Resolve<T> | undefined, onRejected?: Reject) {
return new MyPromise<T>((resolve, reject) => {
const enqueueFulfilled = (value: T) => {
queueMicrotask(() => {
if (onFulfilled) {
try {
const fulfilledValue = onFulfilled(value)
resolve(fulfilledValue as T)
} catch (error) {
reject(error)
}
} else {
resolve(value)
}
})
}

const enqueueRejected = (reason: any) => {
queueMicrotask(() => {
if (onRejected) {
try {
const rejectedValue = onRejected(reason)
resolve(rejectedValue as T)
} catch (error) {
reject(error)
}
} else {
reject(reason)
}
})
}

const lookupTable = {
fulfilled: () => enqueueFulfilled(this.promiseResult as T),
rejected: () => enqueueRejected(this.promiseResult),
pending: () => {
this.resolveQueue.push(enqueueFulfilled)
this.rejectQueue.push(enqueueRejected)
},
}

lookupTable[this.promiseState]()
})
}

finally(onFinally: () => void) {
return this.then(onFinally, onFinally)
}
}