Skip to content

Commit 9c49b2d

Browse files
authoredNov 20, 2024··
refactor(promise): rewrite promise (#175)
1 parent dd171ae commit 9c49b2d

File tree

4 files changed

+526
-479
lines changed

4 files changed

+526
-479
lines changed
 

‎.eslintrc.cjs

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
module.exports = {
22
env: {
33
es2023: true,
4+
node: true,
5+
browser: true,
46
},
57
extends: [
68
'airbnb-base',

‎src/data-structures/promise/index.ts

+117-89
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
// Promises/A+ spec
22
// https://github.com/promises-aplus/promises-spec
3-
4-
import isFunction from 'lodash.isfunction';
53
import { ValueOf } from 'type-fest';
64

75
/**
@@ -48,21 +46,19 @@ interface IMyPromise<T> {
4846
readonly [Symbol.toStringTag]: string;
4947
}
5048

51-
const STATE = {
49+
const STATES = {
5250
PENDING: 'pending',
5351
FULFILLED: 'fulfilled',
5452
REJECTED: 'rejected',
5553
} as const;
5654

57-
type Callback = () => void;
58-
5955
interface PromiseFulfilledResult<T> {
60-
status: typeof STATE.FULFILLED;
56+
status: typeof STATES.FULFILLED;
6157
value: T;
6258
}
6359

6460
interface PromiseRejectedResult {
65-
status: typeof STATE.REJECTED;
61+
status: typeof STATES.REJECTED;
6662
reason: any;
6763
}
6864

@@ -71,16 +67,17 @@ type PromiseSettledResult<T> =
7167
| PromiseRejectedResult;
7268

7369
export class MyPromise<T = any> implements IMyPromise<T> {
74-
#state: ValueOf<typeof STATE> = STATE.PENDING;
70+
#state: ValueOf<typeof STATES> = STATES.PENDING;
7571

76-
#value?: T | PromiseLike<T>;
72+
#value: T | PromiseLike<T> | undefined = undefined;
7773

78-
#reason: any;
74+
#reason: any = undefined;
7975

80-
#onfulfilledCallbacks: Callback[] = [];
76+
#fulfilledCallbacksQueue: (() => void)[] = [];
8177

82-
#onrejectedCallbacks: Callback[] = [];
78+
#rejectedCallbacksQueue: (() => void)[] = [];
8379

80+
// --- Resolve --------------------
8481
static resolve(): MyPromise<void>;
8582
static resolve<T>(value: T): MyPromise<Awaited<T>>;
8683
static resolve<T>(value: T | PromiseLike<T>): MyPromise<Awaited<T>>;
@@ -92,11 +89,13 @@ export class MyPromise<T = any> implements IMyPromise<T> {
9289
return new MyPromise<Awaited<T>>((resolve) => resolve(value as Awaited<T>));
9390
}
9491

92+
// --- Reject --------------------
9593
static reject<T = never>(reason?: any) {
9694
return new MyPromise<T>((_, reject) => reject(reason));
9795
}
9896

99-
static all<T>(values: Iterable<T | PromiseLike<T>>) {
97+
// --- All --------------------
98+
static all<T>(values: Iterable<T | PromiseLike<T>> = []) {
10099
return new MyPromise<Awaited<T>[]>((resolve, reject) => {
101100
handleNonIterable(values);
102101

@@ -127,6 +126,7 @@ export class MyPromise<T = any> implements IMyPromise<T> {
127126
});
128127
}
129128

129+
// --- Race --------------------
130130
static race<T>(values: Iterable<T | PromiseLike<T>> = []) {
131131
return new MyPromise<Awaited<T>>((resolve, reject) => {
132132
handleNonIterable(values);
@@ -137,11 +137,12 @@ export class MyPromise<T = any> implements IMyPromise<T> {
137137
});
138138
}
139139

140+
// --- Any --------------------
140141
static any<T extends readonly unknown[] | []>(
141142
values: T,
142143
): MyPromise<Awaited<T[number]>>;
143144
static any<T>(values: Iterable<T | PromiseLike<T>>): MyPromise<Awaited<T>>;
144-
static any<T>(values: Iterable<T | PromiseLike<T>>) {
145+
static any<T>(values: Iterable<T | PromiseLike<T>> = []) {
145146
return new MyPromise<Awaited<T>>((resolve, reject) => {
146147
handleNonIterable(values);
147148

@@ -173,6 +174,7 @@ export class MyPromise<T = any> implements IMyPromise<T> {
173174
});
174175
}
175176

177+
// --- All Settled --------------------
176178
static allSettled<T extends readonly unknown[] | []>(
177179
values: T,
178180
): MyPromise<{
@@ -185,11 +187,11 @@ export class MyPromise<T = any> implements IMyPromise<T> {
185187
const items = Array.from(values).map((item) =>
186188
MyPromise.resolve(item).then(
187189
(value) => ({
188-
status: STATE.FULFILLED,
190+
status: STATES.FULFILLED,
189191
value,
190192
}),
191193
(reason) => ({
192-
status: STATE.REJECTED,
194+
status: STATES.REJECTED,
193195
reason,
194196
}),
195197
),
@@ -198,6 +200,7 @@ export class MyPromise<T = any> implements IMyPromise<T> {
198200
return MyPromise.all<PromiseSettledResult<Awaited<T>>>(items);
199201
}
200202

203+
// --- With Resolvers --------------------
201204
static withResolvers<T>() {
202205
let resolve!: (value: T | PromiseLike<T>) => void;
203206
let reject!: (reason?: any) => void;
@@ -214,114 +217,135 @@ export class MyPromise<T = any> implements IMyPromise<T> {
214217
};
215218
}
216219

220+
// --- Constructor --------------------
217221
constructor(
218222
executor: (
219223
resolve: (value: T | PromiseLike<T>) => void,
220224
reject: (reason?: any) => void,
221225
) => void,
222226
) {
227+
// Error Handling -----
223228
if (new.target === undefined) {
224229
throw new TypeError(
225230
`${this.constructor.name} constructor cannot be invoked without 'new'`,
226231
);
227232
}
228233

229-
if (typeof executor !== 'function') {
234+
if (!isFunction(executor)) {
230235
throw new TypeError(
231-
`${this.constructor.name} resolver ${typeof executor} is not a function`,
236+
`${this.constructor.name} resolver ${type(executor)} is not a function`,
232237
);
233238
}
234239

235-
try {
236-
executor(this.#resolve.bind(this), this.#reject.bind(this));
237-
} catch (error) {
238-
this.#reject(error);
239-
}
240-
}
240+
// Resolve -----
241+
const internalResolve = (value: T | PromiseLike<T>) => {
242+
if (this.#state !== STATES.PENDING) return;
241243

242-
#resolve(value: T | PromiseLike<T>) {
243-
if (this.#state !== STATE.PENDING) return;
244+
this.#state = STATES.FULFILLED;
245+
this.#value = value;
244246

245-
this.#state = STATE.FULFILLED;
246-
this.#value = value;
247+
this.#fulfilledCallbacksQueue.forEach((callback) => {
248+
callback();
249+
});
247250

248-
executeCallbacks(this.#onfulfilledCallbacks);
249-
this.#onfulfilledCallbacks = [];
250-
}
251+
this.#fulfilledCallbacksQueue = [];
252+
};
251253

252-
#reject(reason?: any) {
253-
if (this.#state !== STATE.PENDING) return;
254+
// Reject -----
255+
const internalReject = (reason: any) => {
256+
if (this.#state !== STATES.PENDING) return;
254257

255-
this.#state = STATE.REJECTED;
256-
this.#reason = reason;
258+
this.#state = STATES.REJECTED;
259+
this.#reason = reason;
257260

258-
executeCallbacks(this.#onrejectedCallbacks);
259-
this.#onfulfilledCallbacks = [];
261+
this.#rejectedCallbacksQueue.forEach((callback) => {
262+
callback();
263+
});
264+
265+
this.#fulfilledCallbacksQueue = [];
266+
};
267+
268+
// Constructor execution -----
269+
try {
270+
executor(internalResolve, internalReject);
271+
} catch (error) {
272+
queueMicrotask(() => {
273+
internalReject(error);
274+
});
275+
}
260276
}
261277

278+
// --- Then --------------------
262279
then<TResult1 = T, TResult2 = never>(
263280
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
264281
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null,
265282
) {
266283
return new MyPromise((resolve, reject) => {
267-
const handleFulfilled = executeHandler(onfulfilled, resolve);
268-
const handleRejected = executeHandler(onrejected, reject);
284+
if (this.#state === STATES.PENDING) {
285+
this.#fulfilledCallbacksQueue.push(() => {
286+
handleFulfilled(this.#value as T);
287+
});
288+
289+
this.#rejectedCallbacksQueue.push(() => {
290+
handleRejected(this.#reason);
291+
});
292+
} else if (this.#state === STATES.FULFILLED) {
293+
if (isThenable(this.#value)) {
294+
this.#value.then(handleFulfilled, handleFulfilled);
295+
} else {
296+
handleFulfilled(this.#value as T);
297+
}
298+
} else {
299+
handleRejected(this.#reason);
300+
}
269301

270-
const strategy = {
271-
[STATE.FULFILLED]: () => {
302+
function handleFulfilled(value: T) {
303+
if (isFunction(onfulfilled)) {
272304
queueMicrotask(() => {
273-
if (isThenable(this.#value)) {
274-
this.#value.then(handleFulfilled);
275-
} else {
276-
handleFulfilled(this.#value);
277-
}
305+
executeCallback(onfulfilled, value);
278306
});
279-
},
280-
[STATE.REJECTED]: () => {
307+
} else {
308+
resolve(value);
309+
}
310+
}
311+
312+
function handleRejected(reason: any) {
313+
if (isFunction(onrejected)) {
281314
queueMicrotask(() => {
282-
handleRejected(this.#reason);
315+
executeCallback(onrejected, reason);
283316
});
284-
},
285-
[STATE.PENDING]: () => {
286-
this.#onfulfilledCallbacks.push(() => handleFulfilled(this.#value));
287-
this.#onrejectedCallbacks.push(() => handleRejected(this.#reason));
288-
},
289-
};
290-
291-
strategy[this.#state]();
292-
293-
function executeHandler(
294-
handler: typeof onfulfilled | typeof onrejected,
295-
resolver: (value: any) => void,
317+
} else {
318+
reject(reason);
319+
}
320+
}
321+
322+
function executeCallback(
323+
callback: typeof onfulfilled | typeof onrejected,
324+
data: any,
296325
) {
297-
return (value: any) => {
298-
try {
299-
if (!isFunction(handler)) {
300-
resolver(value);
301-
302-
return;
303-
}
304-
305-
const result = handler(value);
306-
if (isThenable(result)) {
307-
result.then(resolve, reject);
308-
} else {
309-
resolve(result);
310-
}
311-
} catch (error) {
312-
reject(error);
326+
try {
327+
const result = callback && callback(data);
328+
329+
if (isThenable(result)) {
330+
result.then(resolve, reject);
331+
} else {
332+
resolve(result);
313333
}
314-
};
334+
} catch (error) {
335+
reject(error);
336+
}
315337
}
316338
});
317339
}
318340

341+
// --- Catch --------------------
319342
catch<TResult = never>(
320343
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null,
321344
) {
322345
return this.then(null, onrejected);
323346
}
324347

348+
// --- Finally --------------------
325349
finally(onfinally?: (() => void) | undefined | null) {
326350
return this.then(
327351
(value) => {
@@ -346,13 +370,13 @@ export class MyPromise<T = any> implements IMyPromise<T> {
346370
}
347371
}
348372

373+
// --- Utils --------------------
349374
function handleNonIterable(it: any): void {
350375
if (it[Symbol.iterator]) return;
351376

352-
let type = typeof it;
353-
let prefix = `${type}`;
377+
let prefix = `${type(it)}`;
354378

355-
if (type === 'number' || type === 'boolean') {
379+
if (type(it) === 'number' || type(it) === 'boolean') {
356380
prefix += ` ${it}`;
357381
}
358382

@@ -361,14 +385,18 @@ function handleNonIterable(it: any): void {
361385
);
362386
}
363387

364-
function isThenable<T>(it: any): it is PromiseLike<T> {
365-
return !!(it && isFunction(it.then));
388+
function isThenable(it: any): it is PromiseLike<unknown> {
389+
return Boolean(isObject(it) && isFunction(it.then));
366390
}
367391

368-
function executeCallbacks(queue: Callback[]) {
369-
for (const callback of queue) {
370-
queueMicrotask(() => {
371-
callback();
372-
});
373-
}
392+
function isObject(it: unknown) {
393+
return type(it) === 'object' ? it !== null : isFunction(it);
394+
}
395+
396+
function isFunction(it: unknown): it is (...args: unknown[]) => unknown {
397+
return type(it) === 'function';
398+
}
399+
400+
function type(it: unknown) {
401+
return typeof it;
374402
}

‎src/data-structures/promise/promise.spec.ts

+406-389
Large diffs are not rendered by default.

‎tsconfig.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"extends": "./tsconfig.custom.json",
33
"compilerOptions": {
4-
"lib": ["ESNext"],
4+
"lib": ["ESNext", "DOM"],
55
"module": "esnext",
66
"target": "esnext",
77
"moduleResolution": "bundler",

0 commit comments

Comments
 (0)
Please sign in to comment.