Last active
August 15, 2019 17:13
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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