Skip to content

Instantly share code, notes, and snippets.

@ivan-kleshnin
Forked from vkarpov15/promise.js
Created June 19, 2019 11:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ivan-kleshnin/7d2d899f39c2a1e6c52a7e0890c58330 to your computer and use it in GitHub Desktop.
Save ivan-kleshnin/7d2d899f39c2a1e6c52a7e0890c58330 to your computer and use it in GitHub Desktop.
Simple Promises/A+ Compliant Promise
const assert = (v, err) => {
if (!v) {
throw err;
}
};
let counter = 0;
class Promise {
constructor(executor) {
assert(typeof executor === 'function',
new TypeError('Executor not a function'));
// Internal state.
this.state = 'PENDING';
this.chained = [];
this.value = undefined;
this.id = ++counter;
this.executor = executor;
const { resolve, reject } = this._wrapResolveReject();
try {
// Reject if the executor function throws a sync error
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
// In addition to enforcing that a promise cannot change state once
// it is settled, a promise also cannot change state once you call
// `resolve()` with a promise. Easiest way to enforce this is to
// ensure `resolve()` and `reject()` can only be called once.
_wrapResolveReject() {
let called = false;
const resolve = v => {
if (called) return;
called = true;
this.resolve(v);
};
const reject = err => {
if (called) return;
called = true;
this.reject(err);
};
return { resolve, reject };
}
then(_onFulfilled, _onRejected) {
// Defaults to ensure `onFulfilled` and `onRejected` are always
// functions. `onFulfilled` is a no-op by default...
if (typeof _onFulfilled !== 'function') {
_onFulfilled = (v => v);
}
// and `onRejected` just rethrows the error by default
if (typeof _onRejected !== 'function') {
_onRejected = err => { throw err; };
}
return new Promise((resolve, reject) => {
// Wrap `onFulfilled` and `onRejected` for two reasons:
// consistent async and `try/catch`
const onFulfilled = res => setImmediate(() => {
try {
resolve(_onFulfilled(res));
} catch (err) {
reject(err);
}
});
const onRejected = err => setImmediate(() => {
try {
resolve(_onRejected(err));
} catch (err) {
reject(err);
}
});
if (this.state === 'FULFILLED') return onFulfilled(this.value);
if (this.state === 'REJECTED') return onRejected(this.value);
this.chained.push({ onFulfilled, onRejected });
});
}
resolve(value) {
if (this.state !== 'PENDING') return;
if (value === this) {
return this.reject(TypeError(`Can't resolve promise with itself`));
}
// Is `value` a thenable? If so, fulfill/reject this promise when
// `value` fulfills or rejects. The Promises/A+ spec calls this
// process "assimilating" the other promise (resistance is futile).
const then = this._getThenProperty(value);
if (typeof then === 'function') {
// Important detail: `resolve()` and `reject()` cannot be called
// more than once. This means if `then()` calls `resolve()` with
// a promise that later fulfills and then throws, the promise
// that `then()` returns will be fulfilled.
const { resolve, reject } = this._wrapResolveReject();
try {
return then.call(value, resolve, reject);
} catch (error) {
return reject(error);
}
}
// If `value` is **not** a thenable, transition to fulfilled
this.state = 'FULFILLED';
this.value = value;
this.chained.
forEach(({ onFulfilled }) => setImmediate(onFulfilled, value));
}
reject(v) {
if (this.state !== 'PENDING') return;
this.state = 'REJECTED';
this.value = v;
this.chained.forEach(({ onRejected }) => setImmediate(onRejected, v));
}
_getThenProperty(value) {
if (['object', 'function'].includes(typeof value) && value != null) {
try {
return value.then;
} catch (error) {
// Unlikely edge case that is enforced by Promise/A+ spec
// section 2.3.3.2: if getting `value.then` throws, reject
// immediately.
this.reject(error);
}
}
}
catch(onRejected) {
return this.then(null, onRejected);
}
// ------------------------------------
// The below functionality is **not** covered in the book, but is necessary
// for the es6 promise test suite
// ------------------------------------
finally(onFinally) {
return this.then(
/* onFulfilled */
res => Promise.resolve(onFinally.call(this)).then(() => res),
/* onRejected */
err => Promise.resolve(onFinally.call(this)).then(() => { throw err; })
);
}
static all(arr) {
if (!Array.isArray(arr)) {
return _Promise.reject(new TypeError('all() only accepts an array'));
}
let remaining = arr.length;
if (arr.length === 0) {
return _Promise.resolve([]);
}
let result = [];
return new _Promise((resolve, reject) => {
arr.forEach((p, i) => {
_Promise.resolve(p).then(
res => {
result[i] = res;
--remaining || resolve(result);
},
err => {
reject(err);
});
});
});
}
static race(arr) {
const _Promise = this;
if (!Array.isArray(arr)) {
return _Promise.reject(new TypeError('race() only accepts an array'));
}
return new _Promise((resolve, reject) => {
arr.forEach(p => {
_Promise.resolve(p).then(resolve, reject);
});
});
}
static resolve(v) {
return new this(resolve => resolve(v));
}
static reject(err) {
return new this((resolve, reject) => reject(err));
}
}
module.exports = {
resolved: v => Promise.resolve(v),
rejected: err => Promise.reject(err),
deferred: () => {
let resolve;
let reject;
const promise = new Promise((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
return { resolve, reject, promise };
}
};
@ivan-kleshnin
Copy link
Author

ivan-kleshnin commented Jun 19, 2019

Note for a random casual viewer... It's almost a perfectly balanced solution (readability vs being realitic). Most others I checked were broken in significant ways. Cheers to Valeri for sharing this code!

The only major issue (or rather type of issue) I found is return _Promise.reject(new TypeError('all() only accepts an array')); which should be throw TypeError('all() only accepts an array'). Yes, this should be an immediate sync error! Same goes for other error handling lines. The abscence of distinction between code errors and rejections is a big problem with Promises A+ already. No need to make it worse.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment