├── .gitignore ├── LICENSE ├── MyPromise.js ├── MyPromise.test.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 WebDevSimplified 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MyPromise.js: -------------------------------------------------------------------------------- 1 | const STATE = { 2 | FULFILLED: "fulfilled", 3 | REJECTED: "rejected", 4 | PENDING: "pending", 5 | } 6 | 7 | class MyPromise { 8 | #thenCbs = [] 9 | #catchCbs = [] 10 | #state = STATE.PENDING 11 | #value 12 | #onSuccessBind = this.#onSuccess.bind(this) 13 | #onFailBind = this.#onFail.bind(this) 14 | 15 | constructor(cb) { 16 | try { 17 | cb(this.#onSuccessBind, this.#onFailBind) 18 | } catch (e) { 19 | this.#onFail(e) 20 | } 21 | } 22 | 23 | #runCallbacks() { 24 | if (this.#state === STATE.FULFILLED) { 25 | this.#thenCbs.forEach(callback => { 26 | callback(this.#value) 27 | }) 28 | 29 | this.#thenCbs = [] 30 | } 31 | 32 | if (this.#state === STATE.REJECTED) { 33 | this.#catchCbs.forEach(callback => { 34 | callback(this.#value) 35 | }) 36 | 37 | this.#catchCbs = [] 38 | } 39 | } 40 | 41 | #onSuccess(value) { 42 | queueMicrotask(() => { 43 | if (this.#state !== STATE.PENDING) return 44 | 45 | if (value instanceof MyPromise) { 46 | value.then(this.#onSuccessBind, this.#onFailBind) 47 | return 48 | } 49 | 50 | this.#value = value 51 | this.#state = STATE.FULFILLED 52 | this.#runCallbacks() 53 | }) 54 | } 55 | 56 | #onFail(value) { 57 | queueMicrotask(() => { 58 | if (this.#state !== STATE.PENDING) return 59 | 60 | if (value instanceof MyPromise) { 61 | value.then(this.#onSuccessBind, this.#onFailBind) 62 | return 63 | } 64 | 65 | if (this.#catchCbs.length === 0) { 66 | throw new UncaughtPromiseError(value) 67 | } 68 | 69 | this.#value = value 70 | this.#state = STATE.REJECTED 71 | this.#runCallbacks() 72 | }) 73 | } 74 | 75 | then(thenCb, catchCb) { 76 | return new MyPromise((resolve, reject) => { 77 | this.#thenCbs.push(result => { 78 | if (thenCb == null) { 79 | resolve(result) 80 | return 81 | } 82 | 83 | try { 84 | resolve(thenCb(result)) 85 | } catch (error) { 86 | reject(error) 87 | } 88 | }) 89 | 90 | this.#catchCbs.push(result => { 91 | if (catchCb == null) { 92 | reject(result) 93 | return 94 | } 95 | 96 | try { 97 | resolve(catchCb(result)) 98 | } catch (error) { 99 | reject(error) 100 | } 101 | }) 102 | 103 | this.#runCallbacks() 104 | }) 105 | } 106 | 107 | catch(cb) { 108 | return this.then(undefined, cb) 109 | } 110 | 111 | finally(cb) { 112 | return this.then( 113 | result => { 114 | cb() 115 | return result 116 | }, 117 | result => { 118 | cb() 119 | throw result 120 | } 121 | ) 122 | } 123 | 124 | static resolve(value) { 125 | return new Promise(resolve => { 126 | resolve(value) 127 | }) 128 | } 129 | 130 | static reject(value) { 131 | return new Promise((resolve, reject) => { 132 | reject(value) 133 | }) 134 | } 135 | 136 | static all(promises) { 137 | const results = [] 138 | let completedPromises = 0 139 | return new MyPromise((resolve, reject) => { 140 | for (let i = 0; i < promises.length; i++) { 141 | const promise = promises[i] 142 | promise 143 | .then(value => { 144 | completedPromises++ 145 | results[i] = value 146 | if (completedPromises === promises.length) { 147 | resolve(results) 148 | } 149 | }) 150 | .catch(reject) 151 | } 152 | }) 153 | } 154 | 155 | static allSettled(promises) { 156 | const results = [] 157 | let completedPromises = 0 158 | return new MyPromise(resolve => { 159 | for (let i = 0; i < promises.length; i++) { 160 | const promise = promises[i] 161 | promise 162 | .then(value => { 163 | results[i] = { status: STATE.FULFILLED, value } 164 | }) 165 | .catch(reason => { 166 | results[i] = { status: STATE.REJECTED, reason } 167 | }) 168 | .finally(() => { 169 | completedPromises++ 170 | if (completedPromises === promises.length) { 171 | resolve(results) 172 | } 173 | }) 174 | } 175 | }) 176 | } 177 | 178 | static race(promises) { 179 | return new MyPromise((resolve, reject) => { 180 | promises.forEach(promise => { 181 | promise.then(resolve).catch(reject) 182 | }) 183 | }) 184 | } 185 | 186 | static any(promises) { 187 | const errors = [] 188 | let rejectedPromises = 0 189 | return new MyPromise((resolve, reject) => { 190 | for (let i = 0; i < promises.length; i++) { 191 | const promise = promises[i] 192 | promise.then(resolve).catch(value => { 193 | rejectedPromises++ 194 | errors[i] = value 195 | if (rejectedPromises === promises.length) { 196 | reject(new AggregateError(errors, "All promises were rejected")) 197 | } 198 | }) 199 | } 200 | }) 201 | } 202 | } 203 | 204 | class UncaughtPromiseError extends Error { 205 | constructor(error) { 206 | super(error) 207 | 208 | this.stack = `(in promise) ${error.stack}` 209 | } 210 | } 211 | 212 | module.exports = MyPromise 213 | -------------------------------------------------------------------------------- /MyPromise.test.js: -------------------------------------------------------------------------------- 1 | const MyPromise = require("./MyPromise.js") 2 | // const MyPromise = Promise 3 | 4 | const DEFAULT_VALUE = "default" 5 | 6 | describe("then", () => { 7 | it("with no chaining", () => { 8 | return promise().then(v => expect(v).toEqual(DEFAULT_VALUE)) 9 | }) 10 | 11 | it("with multiple thens for same promise", () => { 12 | const checkFunc = v => expect(v).toEqual(DEFAULT_VALUE) 13 | const mainPromise = promise() 14 | const promise1 = mainPromise.then(checkFunc) 15 | const promise2 = mainPromise.then(checkFunc) 16 | return Promise.allSettled([promise1, promise2]) 17 | }) 18 | 19 | it("with then and catch", () => { 20 | const checkFunc = v => expect(v).toEqual(DEFAULT_VALUE) 21 | const failFunc = v => expect(1).toEqual(2) 22 | const resolvePromise = promise().then(checkFunc, failFunc) 23 | const rejectPromise = promise({ fail: true }).then(failFunc, checkFunc) 24 | return Promise.allSettled([resolvePromise, rejectPromise]) 25 | }) 26 | 27 | it("with chaining", () => { 28 | return promise({ value: 3 }) 29 | .then(v => v * 4) 30 | .then(v => expect(v).toEqual(12)) 31 | }) 32 | }) 33 | 34 | describe("catch", () => { 35 | it("with no chaining", () => { 36 | return promise({ fail: true }).catch(v => expect(v).toEqual(DEFAULT_VALUE)) 37 | }) 38 | 39 | it("with multiple catches for same promise", () => { 40 | const checkFunc = v => expect(v).toEqual(DEFAULT_VALUE) 41 | const mainPromise = promise({ fail: true }) 42 | const promise1 = mainPromise.catch(checkFunc) 43 | const promise2 = mainPromise.catch(checkFunc) 44 | return Promise.allSettled([promise1, promise2]) 45 | }) 46 | 47 | it("with chaining", () => { 48 | return promise({ value: 3 }) 49 | .then(v => { 50 | throw v * 4 51 | }) 52 | .catch(v => expect(v).toEqual(12)) 53 | }) 54 | }) 55 | 56 | describe("finally", () => { 57 | it("with no chaining", () => { 58 | const checkFunc = v => v => expect(v).toBeUndefined() 59 | const successPromise = promise().finally(checkFunc) 60 | const failPromise = promise({ fail: true }).finally(checkFunc) 61 | return Promise.allSettled([successPromise, failPromise]) 62 | }) 63 | 64 | it("with multiple finallys for same promise", () => { 65 | const checkFunc = v => expect(v).toBeUndefined() 66 | const mainPromise = promise() 67 | const promise1 = mainPromise.finally(checkFunc) 68 | const promise2 = mainPromise.finally(checkFunc) 69 | return Promise.allSettled([promise1, promise2]) 70 | }) 71 | 72 | it("with chaining", () => { 73 | const checkFunc = v => v => expect(v).toBeUndefined() 74 | const successPromise = promise() 75 | .then(v => v) 76 | .finally(checkFunc) 77 | const failPromise = promise({ fail: true }) 78 | .then(v => v) 79 | .finally(checkFunc) 80 | return Promise.allSettled([successPromise, failPromise]) 81 | }) 82 | }) 83 | 84 | describe("static methods", () => { 85 | it("resolve", () => { 86 | return MyPromise.resolve(DEFAULT_VALUE).then(v => 87 | expect(v).toEqual(DEFAULT_VALUE) 88 | ) 89 | }) 90 | 91 | it("reject", () => { 92 | return MyPromise.reject(DEFAULT_VALUE).catch(v => 93 | expect(v).toEqual(DEFAULT_VALUE) 94 | ) 95 | }) 96 | 97 | describe("all", () => { 98 | it("with success", () => { 99 | return MyPromise.all([promise({ value: 1 }), promise({ value: 2 })]).then( 100 | v => expect(v).toEqual([1, 2]) 101 | ) 102 | }) 103 | 104 | it("with fail", () => { 105 | return MyPromise.all([promise(), promise({ fail: true })]).catch(v => 106 | expect(v).toEqual(DEFAULT_VALUE) 107 | ) 108 | }) 109 | }) 110 | 111 | it("allSettled", () => { 112 | return MyPromise.allSettled([promise(), promise({ fail: true })]).then(v => 113 | expect(v).toEqual([ 114 | { status: "fulfilled", value: DEFAULT_VALUE }, 115 | { status: "rejected", reason: DEFAULT_VALUE }, 116 | ]) 117 | ) 118 | }) 119 | 120 | describe("race", () => { 121 | it("with success", () => { 122 | return MyPromise.race([ 123 | promise({ value: 1 }), 124 | promise({ value: 2 }), 125 | ]).then(v => expect(v).toEqual(1)) 126 | }) 127 | 128 | it("with fail", () => { 129 | return MyPromise.race([ 130 | promise({ fail: true, value: 1 }), 131 | promise({ fail: true, value: 2 }), 132 | ]).catch(v => expect(v).toEqual(1)) 133 | }) 134 | }) 135 | 136 | describe("any", () => { 137 | it("with success", () => { 138 | return MyPromise.any([promise({ value: 1 }), promise({ value: 2 })]).then( 139 | v => expect(v).toEqual(1) 140 | ) 141 | }) 142 | 143 | it("with fail", () => { 144 | return MyPromise.any([ 145 | promise({ fail: true, value: 1 }), 146 | promise({ value: 2 }), 147 | ]).catch(e => expect(e.errors).toEqual([1, 2])) 148 | }) 149 | }) 150 | }) 151 | 152 | function promise({ value = DEFAULT_VALUE, fail = false } = {}) { 153 | return new MyPromise((resolve, reject) => { 154 | fail ? reject(value) : resolve(value) 155 | }) 156 | } 157 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "custom-promise-library", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "MyPromise.js", 6 | "scripts": { 7 | "test": "jest --watchAll --noStackTrace" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "jest": "^27.5.1" 14 | } 15 | } 16 | --------------------------------------------------------------------------------