Skip to content

Instantly share code, notes, and snippets.

@Quacky2200
Last active August 15, 2019 17:13
Show Gist options
  • Save Quacky2200/98c6e789dfdea21b8a766179b32990a9 to your computer and use it in GitHub Desktop.
Save Quacky2200/98c6e789dfdea21b8a766179b32990a9 to your computer and use it in GitHub Desktop.
My own take on JavaScript promises. It was a challenge but just about got it to work. There's probably a few bugs but the tests should help for debugging and testing
Promise = function(fn) {
var value = undefined;
var stack = [];
var state = 'pending'
var triggered = false;
var error = undefined;
var id = (function (len, chars) {
len = len || 10;
chars = chars || "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var text = "";
for (var i = 0; i < len; i += 1) {
text += chars.charAt(Math.floor(Math.random() * chars.length));
}
return text;
}());
// This is not part of the native promises library and is currently left out
// this.getID = function() {
// return id;
// };
// this.getState = function() {
// return state;
// };
var kindof = function(obj) {
return obj && typeof(obj) == 'object' && obj.constructor.name === 'Promise';
}
var workStack = function() {
// When work is finished, reset parameters for next execution
if (stack.length == 0) {
triggered = false;
stack.type = null;
return;
}
// Get the first task
var work = stack.shift();
if (!work) return workStack()
// If we're starting the wrong chain, stop execution
if (work.type !== stack.type) {
// Put work back for correct stack operation.
stack.push(work);
triggered = false;
stack.type = null;
return;
}
var argument = value;
if (work.type === 'catch') {
// If the work we're doing is part of an exception, move the error
// to the argument, and prepare the stack to continue
argument = error;
error = undefined;
// Remove thrown errors after this happens, errors automatically
// go to a 'resolved' state afterwards, allowing the chain to
// follow through. However, 'then' afterwards is part of the
// caught chain and not the original, and will only run if the
// the catch is executed. See below for more details:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch
value = undefined;
state = 'resolved';
stack.type = 'then';
}
setTimeout(function() {
try {
var result = work.callback(argument);
if (kindof(result)) {
// work returned was promise, we will wait for it.
result.catch(fail).then(then);
} else {
// work received a value
value = result;
fire(stack.type);//workStack();
}
} catch (e) {
fail(e);
}
}, 1);
};
var fire = function(event) {
// Find the correct chain of events, this will allow an exception to
// skip any previous execution similarly to a try/catch situation.
// Anything above that's irrelevant will be deleted.
for(var i = 0; i < stack.length; i++) {
if (stack[i].type == event) {
break;
} else {
delete stack[i];
}
}
// Clean up stack with the correct values
delete stack.type;
stack = Object.values(stack);
stack.type = event;
if (stack.length > 0) {
// There's work to do.
workStack();
} else {
// Take a rest, and allow to be restarted
triggered = false;
}
};
var then = function(val) {
value = val;
if (kindof(value)) {
// received a promise, we will wait for it
value = value.catch(fail).then(then);
} else {
// received a value, start work chain
state = 'resolved';
// Setting this to triggered will prevent 2+ workers on a chain
triggered = true;
fire('then');
}
};
var dispose = function(func) {
delete func;
return;
};
var fail = function(val) {
error = val;
if (kindof(error)) {
// received a promise, we will wait for it
error = value.catch(fail).then(dispose);
} else {
// received a value
state = 'rejected';
triggered = true;
fire('catch');
}
};
this.then = function(cb) {
stack.push({type: 'then', callback: cb});
if (state !== 'rejected' && !error) {
if (!triggered && state !== 'pending') {
// if work has stagnated, restart worker
fire('then');
}
}
return this;
};
this.catch = function(cb) {
stack.push({type: 'catch', callback: cb});
if (!triggered && state == 'rejected') {
// if work has stagnated, restart worker
fire('catch');
}
return this;
};
setTimeout(function() {
try {
// Start work. The function calling then or fail with a value will
// start the process. The worker will restart when required making
// sure that all work is processed.
fn.apply(null, [then, fail]);
} catch (e) {
fail(e);
}
}, 1);
}
Promise.resolve = function(val) {
return new Promise(function(resolve, reject) {
resolve(val);
});
};
Promise.reject = function(val) {
return new Promise(function(resolve, reject) {
reject(val);
});
};
Promise.all = function(work) {
return new Promise(function(resolve, reject) {
var results = [];
for(i = 0; i < work.length; i++) {
(function() {
var key = i.toString()
work[i].then(function(val) {
results[key] = val;
if (Object.keys(results).length === work.length) {
resolve(Object.values(results));
}
}).catch(reject);
}());
}
});
};
var test = false;
if (test) {
// Tests:
// This test is testing against being able to:
// - Immediately use a promise resolver
// - Be able to increment the number from 42 to 43, so that...
// - Use the last edited value in a statement
var a = Promise.resolve(42)
.then((i) => ++i)
.then((x) => console.log('The meaning of life is now', x))
// This test is testing against being able to:
// - immediately use a Promise resolver
// - Be able to calculate/change the outcome of the original value for the next
// chain
// - Be able to full run all functions in a sequence
// - Be able to use a full promise return in a later .then chained function
// - Be able to see/validate the change had taken place
var a = Promise.resolve(42)
.then((i) => ++i)
.then((i) => new Promise(function(resolve) {
console.log('Generating answer to life!');
setTimeout(() => resolve(i), 2500);
})
).then((x) => console.log('The meaning of life is now', x))
// This test is testing against being able to:
// - immediately use a promise resolver
// - Be able to change the value of the value
// - Be able to use resolvers/rejections as a returnable value, and that we are
// not given this in the next function as part of the operating chain.
// - That an error can be thrown using the catch method, and simutaneously be
// able to continue onwards after such an error
var a = Promise.resolve(42)
.then((i) => ++i)
.then((i) => console.log(
'Neo, would you believe me that I can change the environment to suite my ' +
'needs, The answer to life is ' + i + ' but only last week it was 42!'
))
.then(function() {
return Promise.resolve(42)
})
.then(function(b) {
console.log('The meaning of life is now', b);
throw new Error('We\'re still in the matrix');
})
.catch(console.error)
.then(function() {
return console.log('Neo, choose a pill, but once you pick it you don\'t ' +
'get a second chance. Pick wisely');
});
Promise.resolve(console.log('(Neo looks at mirror)\nNeo: Huh?'))
.then(() => Promise.reject(new Error('Midlife crisis inbound')))
.then(() => console.log('This must not run!'))
.catch((e) => console.log('Midlife Crisis? Try quarter life crisis!'))
.then((e) => console.log('This must be able to run afterwards with undefined:', e === undefined))
// The below test should generate [5, 32]. This is testing that the numbers that
// are given in the promises are passed down into the correct resulting order,
// that they are actually included in the file, and does not quit pre-maturely
// (i.e. after one test is done).
Promise.all([
new Promise((r) => setTimeout(() => r(5),2500)),
new Promise((r) => setTimeout(() => r(32), 400))
]).then(function(numbers) {
return console.log('The numbers are:', numbers);
});
}
if (typeof(module) !== 'undefined' && module.exports) {
module.exports = Promise;
} else if (window) {
window.Promise = Promise;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment