Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
promise rework #2454
Browse files Browse the repository at this point in the history
d3nd3 committed Apr 24, 2024
1 parent 92215ed commit 8dc5d49
Showing 2 changed files with 365 additions and 230 deletions.
593 changes: 363 additions & 230 deletions src/jswrap_promise.c
Original file line number Diff line number Diff line change
@@ -12,8 +12,10 @@
*
* ES6 Promise implementation
* ----------------------------------------------------------------------------
* See https://github.com/espruino/Espruino/pull/2454 for better understanding.
*/


#include "jsutils.h"

#if ESPR_NO_PROMISES!=1
@@ -27,8 +29,16 @@
#define JS_PROMISE_CATCH_NAME JS_HIDDEN_CHAR_STR"cat"
#define JS_PROMISE_REMAINING_NAME JS_HIDDEN_CHAR_STR"left"
#define JS_PROMISE_RESULT_NAME JS_HIDDEN_CHAR_STR"res"
#define JS_PROMISE_RESOLVED_NAME "resolved"
#define JS_PROMISE_ISRESOLVED_NAME JS_HIDDEN_CHAR_STR"resolved"
#define JS_PROMISE_PROM_NAME JS_HIDDEN_CHAR_STR"prom"
#define JS_PROMISE_VALUE_NAME JS_HIDDEN_CHAR_STR"value"
#define JS_PROMISE_STATE_NAME JS_HIDDEN_CHAR_STR"state"

enum JS_PROMISE_STATE_ENUM {
JS_PROMISE_STATE_PENDING,
JS_PROMISE_STATE_REJECTED,
JS_PROMISE_STATE_FULFILLED
};

/*JSON{
"type" : "class",
@@ -39,9 +49,8 @@
This is the built-in class for ES6 Promises
*/

void _jswrap_promise_queueresolve(JsVar *promise, JsVar *data);
void _jswrap_promise_queuereject(JsVar *promise, JsVar *data);
void _jswrap_promise_add(JsVar *parent, JsVar *callback, bool resolve);
void _jswrap_promise_resolve(JsVar *prombox, JsVar *data);
void _jswrap_promise_reject(JsVar *prombox, JsVar *data);

bool _jswrap_promise_is_promise(JsVar *promise) {
JsVar *constr = jspGetConstructor(promise);
@@ -50,166 +59,229 @@ bool _jswrap_promise_is_promise(JsVar *promise) {
return isPromise;
}


void _jswrap_promise_resolve_or_reject(JsVar *promise, JsVar *data, JsVar *fn) {
// remove any existing handlers since we already have them in `fn`
// If while we're iterating below a function re-adds to the chain then
// we can execute that later
// https://github.com/espruino/Espruino/issues/894#issuecomment-402553934
jsvObjectRemoveChild(promise, JS_PROMISE_THEN_NAME); // remove 'resolve' and 'reject' handlers
jsvObjectRemoveChild(promise, JS_PROMISE_CATCH_NAME); // remove 'resolve' and 'reject' handlers
JsVar *chainedPromise = jsvObjectGetChildIfExists(promise, "chain");
jsvObjectRemoveChild(promise, "chain"); // unlink chain
// execute handlers from `fn`
JsVar *result = 0;
if (jsvIsArray(fn)) {
JsvObjectIterator it;
jsvObjectIteratorNew(&it, fn);
bool first = true;
while (jsvObjectIteratorHasValue(&it)) {
JsVar *f = jsvObjectIteratorGetValue(&it);
JsVar *v = jspExecuteFunction(f, promise, 1, &data);
if (first) {
first = false;
result = v;
} else jsvUnLock(v);
jsvUnLock(f);
jsvObjectIteratorNext(&it);
}
jsvObjectIteratorFree(&it);
} else if (fn) {
result = jspExecuteFunction(fn, promise, 1, &data);
}

JsVar *exception = jspGetException();
if (exception) {
_jswrap_promise_queuereject(chainedPromise, exception);
jsvUnLock3(exception, result, chainedPromise);
// A single reaction chain - this is recursive until a promise is returned or chain ends. data can be undefined/0
void _jswrap_promise_reaction_call(JsVar *promise, JsVar *reaction, JsVar *data, JsVar *isThen) {
JsVar *exceptionName = jsvFindChildFromString(execInfo.hiddenRoot, JSPARSE_EXCEPTION_VAR);
if (exceptionName) {
jsvUnLock(exceptionName);
return;
}

if (chainedPromise) {
if (_jswrap_promise_is_promise(result)) {
// if we were given a promise, loop its 'then' in here
JsVar *fnres = jsvNewNativeFunction((void (*)(void))_jswrap_promise_queueresolve, JSWAT_VOID|JSWAT_THIS_ARG|(JSWAT_JSVAR<<JSWAT_BITS));
JsVar *fnrej = jsvNewNativeFunction((void (*)(void))_jswrap_promise_queuereject, JSWAT_VOID|JSWAT_THIS_ARG|(JSWAT_JSVAR<<JSWAT_BITS));
if (fnres && fnrej) {
jsvObjectSetChild(fnres, JSPARSE_FUNCTION_THIS_NAME, chainedPromise); // bind 'this'
jsvObjectSetChild(fnrej, JSPARSE_FUNCTION_THIS_NAME, chainedPromise); // bind 'this'
_jswrap_promise_add(result, fnres, true);
_jswrap_promise_add(result, fnrej, false);
JsVar * nextPromBox = jsvObjectGetChildIfExists(reaction, "nextBox");
JsVar * nextProm = jsvObjectGetChildIfExists(nextPromBox, JS_PROMISE_PROM_NAME);
if (nextPromBox) {
if (nextProm) {
bool threw = false;
JsVar * retVal;
JsVar * callback = jsvObjectGetChildIfExists(reaction, "cb");
if (callback) {
JsExecFlags oldExecute = execInfo.execute;
retVal = jspeFunctionCall(callback, 0, promise, false, 1, &data);
execInfo.execute = oldExecute;
JsVar *exception = jspGetException();
if (exception) {
threw = true;
jsvUnLock(retVal);
retVal = exception;
}
jsvUnLock(callback);
}
jsvUnLock2(fnres,fnrej);
} else {
_jswrap_promise_queueresolve(chainedPromise, result);
else {
//pass-through
if (!jsvGetBool(isThen)) {
threw = true;
}
retVal = data;
}
if (threw) _jswrap_promise_reject(nextPromBox, retVal);
else _jswrap_promise_resolve(nextPromBox, retVal);

if (callback) jsvUnLock(retVal);
jsvUnLock(nextProm);
}
jsvUnLock(nextPromBox);
}
jsvUnLock2(result, chainedPromise);
}
void _jswrap_promise_resolve_or_reject_chain(JsVar *promise, JsVar *data, bool resolve) {
const char *eventName = resolve ? JS_PROMISE_THEN_NAME : JS_PROMISE_CATCH_NAME;
if (_jswrap_promise_is_promise(data)) {
jsExceptionHere(JSET_ERROR, "Resolving a Promise with a value that is a Promise is not currently supported");
return;
// Value can be undefined/0
void _jswrap_promise_queue_reaction(JsVar *promise, JsVar *reaction, JsVar *value, bool isThenCb) {
JsVar *fn = jsvNewNativeFunction((void (*)(void))_jswrap_promise_reaction_call,
JSWAT_VOID|JSWAT_THIS_ARG|(JSWAT_JSVAR<<JSWAT_BITS)|(JSWAT_JSVAR<<JSWAT_BITS*2)|(JSWAT_JSVAR<<JSWAT_BITS*3));
if (fn) {
jsvObjectSetChild(fn, JSPARSE_FUNCTION_THIS_NAME, promise); // bind 'this'
JsVar *bIsThenCb = jsvNewFromBool(isThenCb);
JsVar *args[] = {reaction, value, bIsThenCb};
jsiQueueEvents(promise, fn, args, 3);
jsvUnLock2(fn, bIsThenCb);
}
// if we didn't have a catch, traverse the chain looking for one
JsVar *fn = jsvObjectGetChildIfExists(promise, eventName);
if (!fn) {
JsVar *chainedPromise = jsvObjectGetChildIfExists(promise, "chain");
while (chainedPromise) {
fn = jsvObjectGetChildIfExists(chainedPromise, eventName);
if (fn) {
_jswrap_promise_resolve_or_reject(chainedPromise, data, fn);
jsvUnLock2(fn, chainedPromise);
return;
}
JsVar *n = jsvObjectGetChildIfExists(chainedPromise, "chain");
jsvUnLock(chainedPromise);
chainedPromise = n;
}
}
void _jswrap_promise_seal(JsVar *promise, JsVar *data,bool resolving) {
if (resolving) {
jsvObjectSetChildAndUnLock(promise, JS_PROMISE_STATE_NAME, jsvNewFromInteger(JS_PROMISE_STATE_FULFILLED));
} else {
jsvObjectSetChildAndUnLock(promise, JS_PROMISE_STATE_NAME, jsvNewFromInteger(JS_PROMISE_STATE_REJECTED));
}
if (resolve)
jsvObjectSetChild(promise, JS_PROMISE_RESOLVED_NAME, data);
if (fn) {
_jswrap_promise_resolve_or_reject(promise, data, fn);
jsvUnLock(fn);
} else if (!resolve) {
JsVar *previouslyResolved = jsvFindChildFromString(promise, JS_PROMISE_RESOLVED_NAME);
if (!previouslyResolved) {

const char *eventName = resolving ? JS_PROMISE_THEN_NAME : JS_PROMISE_CATCH_NAME;
JsVar *reactions = jsvObjectGetChildIfExists(promise, eventName);
if (!reactions) {
if (!resolving) {
jsExceptionHere(JSET_ERROR, "Unhandled promise rejection: %v", data);
// If there was an exception with a stack trace, pass it through so we can keep adding stack to it
JsVar *stack = 0;
if (jsvIsObject(data) && (stack=jsvObjectGetChildIfExists(data, "stack"))) {
jsvObjectSetChildAndUnLock(execInfo.hiddenRoot, JSPARSE_STACKTRACE_VAR, stack);
}
}
jsvUnLock(previouslyResolved);
} else {
if (jsvIsArray(reactions)) {
JsvObjectIterator it;
jsvObjectIteratorNew(&it, reactions);
while (jsvObjectIteratorHasValue(&it)) {
JsVar *reaction = jsvObjectIteratorGetValue(&it);
_jswrap_promise_queue_reaction(promise,reaction,data,resolving);
jsvUnLock(reaction);
jsvObjectIteratorNext(&it);
}
jsvObjectIteratorFree(&it);
} else if (reactions) {
_jswrap_promise_queue_reaction(promise,reactions,data,resolving);
}
jsvUnLock(reactions);
}
}

void _jswrap_promise_resolve(JsVar *promise, JsVar *data) {
_jswrap_promise_resolve_or_reject_chain(promise, data, true);
void _jswrap_promise_resolve_or_reject(JsVar *prombox, JsVar *data, bool resolving) {
JsVar *isResolved = jsvObjectGetChildIfExists(prombox, JS_PROMISE_ISRESOLVED_NAME);
if (jsvGetBool(isResolved)){
jsvUnLock(isResolved);
return;
}
jsvUnLock(isResolved);
jsvObjectSetChildAndUnLock(prombox, JS_PROMISE_ISRESOLVED_NAME, jsvNewFromBool(true));
JsVar * promise = jsvObjectGetChildIfExists(prombox, JS_PROMISE_PROM_NAME);
if (promise) {
if (jsvIsEqual(data,promise)) {
jsExceptionHere(JSET_ERROR,"Illegal resolving to self");
jsvUnLock(promise);
return;
}

jsvObjectSetChild(promise, JS_PROMISE_VALUE_NAME, data);
if (!resolving || !jsvIsObject(data) ) {
_jswrap_promise_seal(promise, data, resolving);
jsvUnLock(promise);
return;
}
bool isProm = _jswrap_promise_is_promise(data);
bool isThenable = false;
JsVar *then = jsvObjectGetChildIfExists(data,"then");
if (jsvIsFunction(then)) isThenable = true;

if (!isThenable && !isProm) {
_jswrap_promise_seal(promise, data, resolving);
jsvUnLock(promise);
return;
}

JsVar *jsResolve = jsvNewNativeFunction((void (*)(void))_jswrap_promise_resolve, JSWAT_VOID|JSWAT_THIS_ARG|(JSWAT_JSVAR<<JSWAT_BITS));
JsVar *jsReject = jsvNewNativeFunction((void (*)(void))_jswrap_promise_reject, JSWAT_VOID|JSWAT_THIS_ARG|(JSWAT_JSVAR<<JSWAT_BITS));

JsVar *prombox = jsvNewObject();
if (prombox) {
jsvObjectSetChild(prombox, JS_PROMISE_PROM_NAME, promise);
jsvObjectSetChildAndUnLock(prombox,JS_PROMISE_ISRESOLVED_NAME,jsvNewFromBool(false));

if (jsResolve) jsvObjectSetChild(jsResolve, JSPARSE_FUNCTION_THIS_NAME, prombox);
if (jsReject) jsvObjectSetChild(jsReject, JSPARSE_FUNCTION_THIS_NAME, prombox);

if (isThenable) {
JsVar *args[2] = {jsResolve,jsReject};
JsExecFlags oldExecute = execInfo.execute;
jsvUnLock(jspeFunctionCall(then, 0, data, false, 2, args));
execInfo.execute = oldExecute;
jsvUnLock(then);
JsVar *exception = jspGetException();
if (exception) {
_jswrap_promise_reject(prombox, exception);
jsvUnLock(exception);
}
} else {
jsvUnLock(jswrap_promise_then(data,jsResolve,jsReject));
}
jsvUnLock3(jsResolve,jsReject,prombox);
}
jsvUnLock(promise);
}
}

void _jswrap_promise_resolve(JsVar *prombox, JsVar *data) {
_jswrap_promise_resolve_or_reject(prombox, data, true);
}

void _jswrap_promise_reject(JsVar *prombox, JsVar *data) {
_jswrap_promise_resolve_or_reject(prombox, data, false);
}
void _jswrap_promise_queueresolve(JsVar *promise, JsVar *data) {

void _jswrap_promise_queueresolve(JsVar *prombox, JsVar *data) {
JsVar *fn = jsvNewNativeFunction((void (*)(void))_jswrap_promise_resolve, JSWAT_VOID|JSWAT_THIS_ARG|(JSWAT_JSVAR<<JSWAT_BITS));
if (!fn) return;
jsvObjectSetChild(fn, JSPARSE_FUNCTION_THIS_NAME, promise); // bind 'this'
jsiQueueEvents(promise, fn, &data, 1);
jsvObjectSetChild(fn, JSPARSE_FUNCTION_THIS_NAME, prombox); // bind 'this'
jsiQueueEvents(prombox, fn, &data, 1);
jsvUnLock(fn);
}

void _jswrap_promise_reject(JsVar *promise, JsVar *data) {
_jswrap_promise_resolve_or_reject_chain(promise, data, false);
}
void _jswrap_promise_queuereject(JsVar *promise, JsVar *data) {
void _jswrap_promise_queuereject(JsVar *prombox, JsVar *data) {
JsVar *fn = jsvNewNativeFunction((void (*)(void))_jswrap_promise_reject, JSWAT_VOID|JSWAT_THIS_ARG|(JSWAT_JSVAR<<JSWAT_BITS));
if (!fn) return;
jsvObjectSetChild(fn, JSPARSE_FUNCTION_THIS_NAME, promise); // bind 'this'
jsiQueueEvents(promise, fn, &data, 1);
jsvObjectSetChild(fn, JSPARSE_FUNCTION_THIS_NAME, prombox); // bind 'this'
jsiQueueEvents(prombox, fn, &data, 1);
jsvUnLock(fn);
}

void jswrap_promise_all_resolve(JsVar *promise, JsVar *index, JsVar *data) {
JsVarInt remaining = jsvGetIntegerAndUnLock(jsvObjectGetChildIfExists(promise, JS_PROMISE_REMAINING_NAME));
JsVar *arr = jsvObjectGetChildIfExists(promise, JS_PROMISE_RESULT_NAME);
if (arr) {
// set the result
jsvSetArrayItem(arr, jsvGetInteger(index), data);
// Update remaining list
remaining--;
jsvObjectSetChildAndUnLock(promise, JS_PROMISE_REMAINING_NAME, jsvNewFromInteger(remaining));
if (remaining==0) {
_jswrap_promise_queueresolve(promise, arr);
}
jsvUnLock(arr);
void jspromise_resolve(JsVar *promise, JsVar *data) {
// give the promise a prombox - ensure resolve once only in c.
// allows us to accept a promise instead of a prombox.
JsVar *prombox = jsvNewObject();
if (!prombox) {
return;
}
jsvObjectSetChildAndUnLock(prombox, JS_PROMISE_PROM_NAME, promise);
jsvObjectSetChildAndUnLock(prombox,JS_PROMISE_ISRESOLVED_NAME,jsvNewFromBool(false));
_jswrap_promise_queueresolve(prombox, data);
}
void jswrap_promise_all_reject(JsVar *promise, JsVar *data) {
JsVar *arr = jsvObjectGetChildIfExists(promise, JS_PROMISE_RESULT_NAME);
if (arr) {
// if not rejected before
jsvUnLock(arr);
jsvObjectRemoveChild(promise, JS_PROMISE_RESULT_NAME);
_jswrap_promise_queuereject(promise, data);

void jspromise_reject(JsVar *promise, JsVar *data) {
// give the promise a prombox - ensure resolve once only in c.
// allows us to accept a promise instead of a prombox.
JsVar *prombox = jsvNewObject();
if (!prombox) {
return;
}
jsvObjectSetChildAndUnLock(prombox, JS_PROMISE_PROM_NAME, promise);
jsvObjectSetChildAndUnLock(prombox,JS_PROMISE_ISRESOLVED_NAME,jsvNewFromBool(false));
_jswrap_promise_queuereject(prombox, data);
}

JsVar *jspromise_create_prombox(JsVar ** promise) {
JsVar *p = jspNewObject(0, "Promise");
if (!p) return 0;
JsVar *box = jsvNewObject();
if (!box) {
jsvUnLock(p);
return 0;
}
jsvObjectSetChildAndUnLock(p, JS_PROMISE_STATE_NAME, jsvNewFromInteger(JS_PROMISE_STATE_PENDING));

jsvObjectSetChildAndUnLock(box, JS_PROMISE_PROM_NAME, p);
jsvObjectSetChildAndUnLock(box,JS_PROMISE_ISRESOLVED_NAME,jsvNewFromBool(false));

*promise = p;
return box;
}
/// Create a new promise
JsVar *jspromise_create() {
return jspNewObject(0, "Promise");
}

/// Resolve the given promise
void jspromise_resolve(JsVar *promise, JsVar *data) {
_jswrap_promise_queueresolve(promise, data);
}

/// Reject the given promise
void jspromise_reject(JsVar *promise, JsVar *data) {
_jswrap_promise_queuereject(promise, data);
}


/*JSON{
"type" : "constructor",
"class" : "Promise",
@@ -226,28 +298,73 @@ Create a new Promise. The executor function is executed immediately (before the
constructor even returns) and
*/
JsVar *jswrap_promise_constructor(JsVar *executor) {
JsVar *obj = jspromise_create();
if (obj) {
// create resolve and reject
JsVar *args[2] = {
jsvNewNativeFunction((void (*)(void))_jswrap_promise_queueresolve, JSWAT_VOID|JSWAT_THIS_ARG|(JSWAT_JSVAR<<JSWAT_BITS)),
jsvNewNativeFunction((void (*)(void))_jswrap_promise_queuereject, JSWAT_VOID|JSWAT_THIS_ARG|(JSWAT_JSVAR<<JSWAT_BITS))
};
// bind 'this' to functions
if (args[0]) jsvObjectSetChild(args[0], JSPARSE_FUNCTION_THIS_NAME, obj);
if (args[1]) jsvObjectSetChild(args[1], JSPARSE_FUNCTION_THIS_NAME, obj);
if (!executor) {
jsExceptionHere(JSET_ERROR,"Executor function required in promise constructor");
return 0;
}
JsVar * promise;
JsVar *promBox = jspromise_create_prombox(&promise);
if (!promBox) return 0;
if (promise) {
//this=prombox, data
JsVar *resolve = jsvNewNativeFunction((void (*)(void))_jswrap_promise_queueresolve, JSWAT_VOID|JSWAT_THIS_ARG|(JSWAT_JSVAR<<JSWAT_BITS));
JsVar *reject = jsvNewNativeFunction((void (*)(void))_jswrap_promise_queuereject, JSWAT_VOID|JSWAT_THIS_ARG|(JSWAT_JSVAR<<JSWAT_BITS));

// 'this' == prombox.
if (resolve) jsvObjectSetChild(resolve, JSPARSE_FUNCTION_THIS_NAME, promBox);
if (reject) jsvObjectSetChild(reject, JSPARSE_FUNCTION_THIS_NAME, promBox);

JsVar *args[2] = {resolve,reject};
// call the executor
//emulates try

JsExecFlags oldExecute = execInfo.execute;
if (executor) jsvUnLock(jspeFunctionCall(executor, 0, obj, false, 2, args));
jsvUnLock(jspeFunctionCall(executor, 0, promise, false, 2, args));
execInfo.execute = oldExecute;
jsvUnLockMany(2, args);

JsVar *exception = jspGetException();
if (exception) {
_jswrap_promise_queuereject(obj, exception);
_jswrap_promise_queuereject(promBox, exception);
jsvUnLock(exception);
}
jsvUnLock2(resolve,reject);
}
jsvUnLock(promBox);
return promise;
}

void jswrap_promise_all_resolve(JsVar *prombox, JsVar *index, JsVar *data) {
JsVar * promise = jsvObjectGetChildIfExists(prombox, JS_PROMISE_PROM_NAME);
if (promise) {
JsVarInt remaining = jsvGetIntegerAndUnLock(jsvObjectGetChildIfExists(promise, JS_PROMISE_REMAINING_NAME));
JsVar *arr = jsvObjectGetChildIfExists(promise, JS_PROMISE_RESULT_NAME);
if (arr) {
// set the result
jsvSetArrayItem(arr, jsvGetInteger(index), data);
// Update remaining list
remaining--;
jsvObjectSetChildAndUnLock(promise, JS_PROMISE_REMAINING_NAME, jsvNewFromInteger(remaining));
if (remaining==0) {
_jswrap_promise_queueresolve(prombox, arr);
}
jsvUnLock(arr);
}
jsvUnLock(promise);
}
}

void jswrap_promise_all_reject(JsVar *prombox, JsVar *data) {
JsVar * promise = jsvObjectGetChildIfExists(prombox, JS_PROMISE_PROM_NAME);
if (promise) {
JsVar *arr = jsvObjectGetChildIfExists(promise, JS_PROMISE_RESULT_NAME);
if (arr) {
// if not rejected before
jsvUnLock(arr);
jsvObjectRemoveChild(promise, JS_PROMISE_RESULT_NAME);
_jswrap_promise_queuereject(prombox, data);
}
jsvUnLock(promise);
}
return obj;
}

/*JSON{
@@ -270,50 +387,51 @@ JsVar *jswrap_promise_all(JsVar *arr) {
jsExceptionHere(JSET_TYPEERROR, "Expecting something iterable, got %t", arr);
return 0;
}
JsVar *promise = jspNewObject(0, "Promise");
if (!promise) return 0;
JsVar *reject = jsvNewNativeFunction((void (*)(void))jswrap_promise_all_reject, JSWAT_VOID|JSWAT_THIS_ARG|(JSWAT_JSVAR<<JSWAT_BITS));
if (!reject) return promise; // out of memory error
jsvObjectSetChild(reject, JSPARSE_FUNCTION_THIS_NAME, promise); // bind 'this'
JsVar *promiseResults = jsvNewEmptyArray();
int promiseIndex = 0;
int promisesComplete = 0;
JsvObjectIterator it;
jsvObjectIteratorNew(&it, arr);
while (jsvObjectIteratorHasValue(&it)) {
JsVar *p = jsvObjectIteratorGetValue(&it);
if (_jswrap_promise_is_promise(p)) {
JsVar *resolve = jsvNewNativeFunction((void (*)(void))jswrap_promise_all_resolve, JSWAT_VOID|JSWAT_THIS_ARG|(JSWAT_JSVAR<<JSWAT_BITS)|(JSWAT_JSVAR<<(JSWAT_BITS*2)));
// NOTE: we use (this,JsVar,JsVar) rather than an int to avoid #2377 on emscripten, since that argspec is already used for forEach/otehrs
// bind the index variable
JsVar *indexVar = jsvNewFromInteger(promiseIndex);
jsvAddFunctionParameter(resolve, 0, indexVar);
jsvUnLock(indexVar);
jsvObjectSetChild(resolve, JSPARSE_FUNCTION_THIS_NAME, promise); // bind 'this'
jsvUnLock2(jswrap_promise_then(p, resolve, reject), resolve);
} else {
jsvSetArrayItem(promiseResults, promiseIndex, p);
promisesComplete++;
JsVar * promise;
JsVar *promBox = jspromise_create_prombox(&promise);
if (!promBox) return 0;
if (promise) {
JsVar *reject = jsvNewNativeFunction((void (*)(void))jswrap_promise_all_reject, JSWAT_VOID|JSWAT_THIS_ARG|(JSWAT_JSVAR<<JSWAT_BITS));
if (!reject) return promise; // out of memory error
jsvObjectSetChild(reject, JSPARSE_FUNCTION_THIS_NAME, promBox); // bind 'this'
JsVar *promiseResults = jsvNewEmptyArray();
int promiseIndex = 0;
int promisesComplete = 0;
JsvObjectIterator it;
jsvObjectIteratorNew(&it, arr);
while (jsvObjectIteratorHasValue(&it)) {
JsVar *p = jsvObjectIteratorGetValue(&it);
if (_jswrap_promise_is_promise(p)) {
JsVar *resolve = jsvNewNativeFunction((void (*)(void))jswrap_promise_all_resolve, JSWAT_VOID|JSWAT_THIS_ARG|(JSWAT_JSVAR<<JSWAT_BITS)|(JSWAT_JSVAR<<(JSWAT_BITS*2)));
// NOTE: we use (this,JsVar,JsVar) rather than an int to avoid #2377 on emscripten, since that argspec is already used for forEach/otehrs
// bind the index variable
JsVar *indexVar = jsvNewFromInteger(promiseIndex);
jsvAddFunctionParameter(resolve, 0, indexVar);
jsvUnLock(indexVar);
jsvObjectSetChild(resolve, JSPARSE_FUNCTION_THIS_NAME, promBox); // bind 'this'
jsvUnLock2(jswrap_promise_then(p, resolve, reject), resolve);
} else {
jsvSetArrayItem(promiseResults, promiseIndex, p);
promisesComplete++;
}
jsvUnLock(p);
promiseIndex++;
jsvObjectIteratorNext(&it);
}
jsvUnLock(p);
promiseIndex++;
jsvObjectIteratorNext(&it);
}
jsvObjectIteratorFree(&it);
if (promisesComplete==promiseIndex) { // already all sorted - return a resolved promise
jsvUnLock(promise);
promise = jswrap_promise_resolve(promiseResults);
jsvUnLock(promiseResults);
} else { // return our new promise that will resolve when everything is done
jsvObjectSetChildAndUnLock(promise, JS_PROMISE_REMAINING_NAME, jsvNewFromInteger(promiseIndex-promisesComplete));
jsvObjectSetChildAndUnLock(promise, JS_PROMISE_RESULT_NAME, promiseResults);
jsvObjectIteratorFree(&it);
if (promisesComplete==promiseIndex) { // already all sorted - return a resolved promise
jsvUnLock(promise);
promise = jswrap_promise_resolve(promiseResults);
jsvUnLock(promiseResults);
} else { // return our new promise that will resolve when everything is done
jsvObjectSetChildAndUnLock(promise, JS_PROMISE_REMAINING_NAME, jsvNewFromInteger(promiseIndex-promisesComplete));
jsvObjectSetChildAndUnLock(promise, JS_PROMISE_RESULT_NAME, promiseResults);
}
jsvUnLock2(reject,promBox);
}

jsvUnLock(reject);
return promise;
}


/*JSON{
"type" : "staticmethod",
"class" : "Promise",
@@ -343,9 +461,12 @@ JsVar *jswrap_promise_resolve(JsVar *data) {
if (promise) return promise;
}
// otherwise the returned promise will be fulfilled with the value.
promise = jspromise_create();
if (!promise) return 0;
jspromise_resolve(promise, data);
JsVar *promBox = jspromise_create_prombox(&promise);
if (!promBox) return 0;
if (promise) {
jspromise_resolve(promBox, data);
}
jsvUnLock(promBox);
return promise;
}

@@ -362,65 +483,53 @@ JsVar *jswrap_promise_resolve(JsVar *data) {
}
Return a new promise that is already rejected (at idle it'll call `.catch`)
*/
/*
Promise.reject()
*/
JsVar *jswrap_promise_reject(JsVar *data) {
JsVar *promise = jspromise_create();
if (!promise) return 0;
jspromise_reject(promise, data);
// otherwise the returned promise will be fulfilled with the value.
JsVar * promise;
JsVar *promBox = jspromise_create_prombox(&promise);
if (!promBox) return 0;
if (promise) {
jspromise_reject(promBox, data);
}
jsvUnLock(promBox);
return promise;
}

void _jswrap_promise_add(JsVar *parent, JsVar *callback, bool resolve) {
if (!jsvIsFunction(callback)) {
jsExceptionHere(JSET_TYPEERROR, "Callback is not a function");
return;
}

bool resolveImmediately = false;
JsVar *resolveImmediatelyValue = 0;

if (resolve) {
// Check to see if promise has already been resolved
/* Note: we use jsvFindChildFromString not ObjectGetChild so we get the name.
* If we didn't then we wouldn't know if it was resolved, but with undefined */
JsVar *resolved = jsvFindChildFromString(parent, JS_PROMISE_RESOLVED_NAME);
if (resolved) {
resolveImmediately = true;
resolveImmediatelyValue = jsvSkipNameAndUnLock(resolved);
}
}

/*
Reactions
*/
JsVar *_jswrap_promise_new_reaction(JsVar *nextPromBox, JsVar *callback) {
JsVar *reaction = jsvNewObject();
if (!reaction) return 0;
jsvObjectSetChild(reaction, "cb", callback);
jsvObjectSetChild(reaction, "nextBox", nextPromBox);
return reaction;
}

void _jswrap_promise_add_reaction(JsVar *parent, JsVar * nextPromBox, JsVar *callback, bool resolve) {
// reaction = [{cb:,box:},...]
JsVar *reaction = _jswrap_promise_new_reaction(nextPromBox,callback);
if (!reaction) return;
const char *name = resolve ? JS_PROMISE_THEN_NAME : JS_PROMISE_CATCH_NAME;
JsVar *c = jsvObjectGetChildIfExists(parent, name);
if (!c) {
jsvObjectSetChild(parent, name, callback);
jsvObjectSetChild(parent, name, reaction);
} else {
if (jsvIsArray(c)) {
jsvArrayPush(c, callback);
jsvArrayPush(c, reaction);
} else {
JsVar *fns[2] = {c,callback};
JsVar *fns[2] = {c,reaction};
JsVar *arr = jsvNewArray(fns, 2);
jsvObjectSetChild(parent, name, arr);
jsvUnLock(arr);
jsvObjectSetChildAndUnLock(parent, name, arr);
}
jsvUnLock(c);
}

if (resolveImmediately) { // If so, queue a resolve event
_jswrap_promise_queueresolve(parent, resolveImmediatelyValue);
jsvUnLock(resolveImmediatelyValue);
}
}

static JsVar *jswrap_promise_get_chained_promise(JsVar *parent) {
JsVar *chainedPromise = jsvObjectGetChildIfExists(parent, "chain");
if (!chainedPromise) {
chainedPromise = jspNewObject(0, "Promise");
jsvObjectSetChild(parent, "chain", chainedPromise);
}
return chainedPromise;
jsvUnLock(reaction);
}

/*JSON{
"type" : "method",
"class" : "Promise",
@@ -436,12 +545,36 @@ static JsVar *jswrap_promise_get_chained_promise(JsVar *parent) {
}
*/
JsVar *jswrap_promise_then(JsVar *parent, JsVar *onFulfilled, JsVar *onRejected) {
_jswrap_promise_add(parent, onFulfilled, true);
if (onRejected)
_jswrap_promise_add(parent, onRejected, false);
return jswrap_promise_get_chained_promise(parent);
JsVar * nextProm;
JsVar *nextPromBox = jspromise_create_prombox(&nextProm);
if (!nextPromBox) return 0;
if (nextProm) {
if (!jsvIsFunction(onFulfilled)) onFulfilled = 0;
if (!jsvIsFunction(onRejected)) onRejected = 0;
JsVar *state = jsvObjectGetChildIfExists(parent, JS_PROMISE_STATE_NAME);
if (state) {
int s = jsvGetIntegerAndUnLock(state);
if (s == JS_PROMISE_STATE_PENDING) {
// Create reaction and attach to promise.
_jswrap_promise_add_reaction(parent, nextPromBox, onFulfilled, true);
_jswrap_promise_add_reaction(parent, nextPromBox, onRejected, false);
} else {
// case: Already resolved, fire Reaction.
JsVar *resolveNowCallback = s == JS_PROMISE_STATE_FULFILLED ? onFulfilled : onRejected;
JsVar *reaction = _jswrap_promise_new_reaction(nextPromBox,resolveNowCallback);
if (reaction) {
JsVar *value = jsvObjectGetChildIfExists(parent, JS_PROMISE_VALUE_NAME);
// Already resolved, go straight to firing reaction.
_jswrap_promise_queue_reaction(parent,reaction,value,false);
if (value) jsvUnLock(value);
jsvUnLock(reaction);
}
}
} //state
}
jsvUnLock(nextPromBox);
return nextProm;
}

/*JSON{
"type" : "method",
"class" : "Promise",
@@ -455,7 +588,7 @@ JsVar *jswrap_promise_then(JsVar *parent, JsVar *onFulfilled, JsVar *onRejected)
}
*/
JsVar *jswrap_promise_catch(JsVar *parent, JsVar *onRejected) {
_jswrap_promise_add(parent, onRejected, false);
return jswrap_promise_get_chained_promise(parent);
return jswrap_promise_then(parent,0,onRejected);
}

#endif // ESPR_NO_PROMISES
2 changes: 2 additions & 0 deletions src/jswrap_promise.h
Original file line number Diff line number Diff line change
@@ -21,6 +21,8 @@

/// Create a new promise
JsVar *jspromise_create();
/// Create a promise and put it inside a promise box
JsVar *jspromise_create_prombox(JsVar ** promise);
/// Resolve the given promise
void jspromise_resolve(JsVar *promise, JsVar *data);
/// Reject the given promise

0 comments on commit 8dc5d49

Please sign in to comment.