Skip to content

Instantly share code, notes, and snippets.

@tinkerer-red
Last active April 24, 2024 19:43
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tinkerer-red/49bb859723afdd5c3c89fe836009f7cc to your computer and use it in GitHub Desktop.
Save tinkerer-red/49bb859723afdd5c3c89fe836009f7cc to your computer and use it in GitHub Desktop.
Javescript Promises in GameMaker's GML
#macro PROMISE_MAX_TIME (1/60 * 1_000 * 1_000) * (1/16) //the max time in milli seconds to spend on the promises, default is 1/16 of frame time of a 60 fps game
enum PROMISE_STATE {
PENDING,
RESOLVED,
REJECTED,
PAUSED,
};
function __PromiseNamespace__() {
//used to help evacuate the global struct so the variables are not easily accessed by users
static __global = {
promises_time_source : time_source_create(time_source_game, 1, time_source_units_frames, function() {
static __global = __PromiseNamespace__()
//handle all of the currently active processes
var _time_to_live = get_timer() + PROMISE_MAX_TIME;
var _i=0; repeat(array_length(__global.active_promises)) {
var _promise = __global.active_promises[_i];
_promise.Execute(_time_to_live);
if (_promise.state == PROMISE_STATE.RESOLVED)
|| (_promise.state == PROMISE_STATE.REJECTED) {
array_delete(__global.active_promises, _i, 1);
_i-=1;
}
//early out
if (get_timer() >= _time_to_live) {
break;
}
_i+=1;}//end repeat loop
//if we have completed all promises
if (array_length(__global.active_promises) == 0) {
time_source_pause(__global.promises_time_source)
}
}, [], -1),
active_promises : [],
};
return __global;
}
function Promise(_resolve_callback=undefined, _reject_callback=function(_reason){ show_debug_message("Promise Failed with error : "+_reason) }) constructor {
// State and value of the promise
executors = [];
state = PROMISE_STATE.PENDING;
value = undefined;
reason = undefined;
on_resolved = _resolve_callback;
on_rejected = _reject_callback;
static Then = function(_executor) {
var _this_promise = (is_instanceof(self, Promise)) ? self : new Promise();
array_push(_this_promise.executors, _executor);
return _this_promise;
};
static Catch = function(_rejected_callback) {
var _this_promise = (is_instanceof(self, Promise)) ? self : new Promise();
_this_promise.on_rejected = _rejected_callback;
return _this_promise;
};
static Finally = function(_resolved_callback) {
var _this_promise = (is_instanceof(self, Promise)) ? self : new Promise();
_this_promise.on_resolved = _resolved_callback;
return _this_promise;
};
static Resolve = function(_value) {
var _this_promise = (is_instanceof(self, Promise)) ? self : new Promise();
_this_promise.state = PROMISE_STATE.PENDING;
_this_promise.value = _value;
return _this_promise;
};
static Reject = function(_reason) {
var _this_promise = (is_instanceof(self, Promise)) ? self : new Promise();
_this_promise.state = PROMISE_STATE.REJECTED;
_this_promise.reason = _reason;
return _this_promise;
};
#region Promise Parenting
static All = function(_arr_of_promises) {
var _parent_promise = new Promise();
_parent_promise.state = PROMISE_STATE.PAUSED;
_parent_promise.sub_promises = _arr_of_promises;
_parent_promise.resolved_count = 0;
_parent_promise.value = [];
var _i=0; repeat(array_length(_arr_of_promises)) {
var _promise = _arr_of_promises[_i];
_promise.parent = _parent_promise;
_promise.index_in_parent = _i;
_promise.Then(method(_promise, function(_value){
parent.value[index_in_parent] = _value;
parent.resolved_count += 1;
if (parent.resolved_count == array_length(parent.sub_promises)) {
parent.state = PROMISE_STATE.PENDING;
}
return _value;
}))
.Catch(method(_parent_promise, function(_reason) {
if (state != PROMISE_STATE.REJECTED) {
state = PROMISE_STATE.REJECTED;
reason = _reason;
}
}));
_i+=1;}//end repeat loop
return _parent_promise;
};
//wait for all to finish, then get reports
static AllSettled = function(_arr_of_promises) {
var _parent_promise = new Promise();
_parent_promise.state = PROMISE_STATE.PAUSED;
_parent_promise.sub_promises = _arr_of_promises;
_parent_promise.resolved_count = 0;
_parent_promise.value = [];
var _i=0; repeat(array_length(_arr_of_promises)) {
var _promise = _arr_of_promises[_i];
_promise.parent = _parent_promise;
_promise.index_in_parent = _i;
_promise.Then(method(_promise, function(_value){
parent.value[index_in_parent] = { status: "RESOLVED", value: _value };
parent.resolved_count += 1;
if (parent.resolved_count == array_length(parent.sub_promises)) {
parent.state = PROMISE_STATE.PENDING;
}
return _value;
}))
.Catch(method(_promise, function(_reason) {
var _error = (is_struct(_reason)) ? _reason.message : _reason;
parent.value[index_in_parent] = { status: "rejected", reason: _error };
parent.resolved_count += 1;
if (parent.resolved_count == array_length(parent.sub_promises)) {
parent.state = PROMISE_STATE.PENDING;
}
}));
_i+=1;}//end repeat loop
return _parent_promise;
};
//if any succeed
static Any = function(_arr_of_promises) {
var _parent_promise = new Promise();
_parent_promise.state = PROMISE_STATE.PAUSED;
_parent_promise.sub_promises = _arr_of_promises;
_parent_promise.resolved_count = 0;
_parent_promise.value = [];
var _i=0; repeat(array_length(_arr_of_promises)) {
var _promise = _arr_of_promises[_i];
_promise.parent = _parent_promise;
_promise.index_in_parent = _i;
_promise.Then(method(_parent_promise, function(_value) {
state = PROMISE_STATE.PENDING;
value = _value;
return _value;
}))
.Catch(method(_parent_promise, function(_reason){
resolved_count += 1;
if (resolved_count == array_length(sub_promises)) {
state = PROMISE_STATE.REJECTED;
reason = _reason;
}
}));
_i+=1;}//end repeat loop
return _parent_promise;
};
//first come first server
static Race = function(_arr_of_promises) {
var _parent_promise = new Promise();
_parent_promise.state = PROMISE_STATE.PAUSED;
_parent_promise.sub_promises = _arr_of_promises;
_parent_promise.resolved_count = 0;
_parent_promise.value = [];
var _i=0; repeat(array_length(_arr_of_promises)) {
var _promise = _arr_of_promises[_i];
_promise.parent = _parent_promise;
_promise.index_in_parent = _i;
_promise.Then(method(_parent_promise, function(_value) {
if (state == PROMISE_STATE.PAUSED) {
state = PROMISE_STATE.RESOLVED;
value = _value;
}
return _value;
}))
.Catch(method(_parent_promise, function(_reason) {
if (state == PROMISE_STATE.PAUSED) {
state = PROMISE_STATE.REJECTED;
reason = _reason;
}
}));
_i+=1;}//end repeat loop
return _parent_promise;
};
#endregion
//note providing no start time will force the promise to execute all sub processes
static Execute = function(_time_to_live=infinity) {
if (state == PROMISE_STATE.PAUSED) {
return true;
}
if (state == PROMISE_STATE.PENDING) {
if (array_length(executors)) {
try {
var _j=0; repeat(array_length(executors)) {
var _executor = executors[0];
var _val = _executor(value);
if (!is_undefined(_val)) {
array_delete(executors, 0, 1);
value = (is_undefined(_val)) ? value : _val;
}
var _length = array_length(executors);
if (_length == 0) {
state = PROMISE_STATE.RESOLVED;
}
//if (_length == 1)
//&& (on_resolved == undefined) {
// state = PROMISE_STATE.RESOLVED;
//}
//early out
if (get_timer() >= _time_to_live) {
break;
}
_j+=1;}//end repeat loop
}
catch (_error) {
state = PROMISE_STATE.REJECTED;
reason = _error;
}
}
else {
state = PROMISE_STATE.RESOLVED;
}
}
if (state == PROMISE_STATE.RESOLVED) {
if (on_resolved != undefined) {
on_resolved(value);
}
}
if (state == PROMISE_STATE.REJECTED) {
if (on_rejected != undefined) {
var _error = (is_struct(reason)) ? reason.message : reason;
on_rejected(_error);
}
}
return true;
}
var _promise = self;
#region Private
static __promise_id = 0;
__promise_id+=1;
_promise.promise_id = __promise_id;
static __global = __PromiseNamespace__();
time_source_start(__global.promises_time_source);
array_push(__global.active_promises, _promise);
#endregion
return _promise;
}
//init statics
new Promise();
#region Promise
//Promise
new Promise(function(_value){
show_debug_message("Promise Working correctly\n")
},
function(_error){
show_debug_message("Promise Failed with error :: "+_error)
})
///
/// NOTE: Returning undefined inside .Then() will postpone the progression of the promise.
///
Promise.Then(function(_value){
show_debug_message("Promise.Then Working correctly\n")
return 10;
})
Promise.Resolve("Resolving")
.Finally(function(_value){
show_debug_message("Promise.Resolve Working correctly\n")
})
//Promise with then
Promise.Reject("Rejection intentional")
.Catch(function(_reason){
show_debug_message("Promise.Reject Working correctly\n")
})
//Promise with finally
Promise.Resolve(10)
.Finally(function(_value){
show_debug_message(_value);
show_debug_message("Promise.Finally Working correctly\n")
})
.Catch(function(_reason) {
show_debug_message("Promise.Then :: FAILED\n");
})
//Promise with catch
Promise.Then(function(_value){
throw "intentionally throwing error to test .catch"
return _value;
})
.Finally(function(_value) {
show_debug_message("Promise.Then :: FAILED\n");
})
.Catch(function(_reason){
show_debug_message("Promise failed with reason: " + _reason);
show_debug_message("Promise.Catch Working correctly\n")
})
#endregion
#region Promise.All
//success
var p1 = Promise.Resolve(1);
var p2 = Promise.Resolve(2);
Promise.All([p1, p2])
.Then(function(_value) {
show_debug_message("All Promises resolved with values: " + string(_value));
show_debug_message("Promise.All.Then Working correctly\n");
return _value;
})
.Finally(function(_value) {
show_debug_message("All Promises resolved with values: " + string(_value));
show_debug_message("Promise.All.Finally Working correctly\n")
})
.Catch(function(_reason) {
show_debug_message("Promise.All failed with reason: " + _reason);
show_debug_message("Promise.All.Finally :: FAILED\n");
});
//failure
var p1 = Promise.Resolve(3);
var p2 = Promise.Reject("error!");
Promise.All([p1, p2])
.Then(function(_values) {
show_debug_message("All Promises resolved with values: " + string(_values));
})
.Finally(function(_value) {
show_debug_message("Promise.All.Catch :: FAILED\n");
})
.Catch(function(_reason) {
show_debug_message("Promise.All failed with reason: " + _reason);
show_debug_message("Promise.All.Catch Working correctly\n")
});
#endregion
#region Promise.AllSettled
var p1 = Promise.Resolve(10);
var p2 = Promise.Reject("error");
Promise.AllSettled([p1, p2])
.Then(function(_results) {
show_debug_message("All Promises settled with results: " + string(_results));
show_debug_message("Promise.AllSettled.Then Working correctly\n");
return _results;
})
.Finally(function(_results) {
show_debug_message("All Promises settled with results: " + string(_results));
show_debug_message("Promise.AllSettled.Finally Working correctly\n")
})
.Catch(function(_reason) {
show_debug_message("Promise.AllSettled.Finally :: FAILED\n");
});
//Expected Output: "All Promises settled with results: [{status: 'fulfilled', value: 10}, {status: 'rejected', reason: 'error'}]"
///
/// NOTE: it is not possible to reach a catch statement when using Promise.AllSettled
///
#endregion
#region Promise.Any
//resolve
var p1 = Promise.Reject("error1");
var p2 = Promise.Resolve(20);
Promise.Any([p1, p2])
.Then(function(_value) {
show_debug_message("At least one Promise resolved with value: " + string(_value));
show_debug_message("Promise.Any.Then Working correctly\n");
return _value;
})
.Finally(function(_value) {
show_debug_message("At least one Promise resolved with value: " + string(_value));
show_debug_message("Promise.Any.Finally Working correctly\n");
})
.Catch(function(_reason) {
show_debug_message("Promise.Any.Finally :: FAILED\n");
});
//reject
var p1 = Promise.Reject("error1");
var p2 = Promise.Reject("error2");
Promise.Any([p1, p2])
.Then(function(_value) {
show_debug_message("At least one Promise resolved with value: " + string(_value));
return _value;
})
.Finally(function(_value) {
show_debug_message("Promise.Any.Catch :: FAILED\n");
})
.Catch(function(_reason) {
show_debug_message("All Promises failed: " + _reason);
show_debug_message("Promise.Any.Catch Working correctly\n")
});
#endregion
#region Promise.Race
//resolve
var p1 = Promise.Resolve(10);
var p2 = Promise.Reject("error");
Promise.Race([p1, p2])
.Then(function(value) {
show_debug_message("First resolved Promise value: " + string(value));
show_debug_message("Promise.Race.Then Working correctly\n")
})
.Finally(function(value) {
show_debug_message("First resolved Promise value: " + string(value));
show_debug_message("Promise.Race.Finally Working correctly\n")
})
//reject
var p1 = Promise.Reject("error");
var p2 = Promise.Resolve(20);
Promise.Race([p1, p2])
.Then(function(value) {
show_debug_message("First resolved Promise value: " + string(value));
})
.Catch(function(reason) {
show_debug_message("First settled Promise was rejected: " + reason);
show_debug_message("Promise.Race.Catch Working correctly\n")
});
#endregion
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment