Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement emscripten_promise_any in promise.h #19153

Merged
merged 3 commits into from
Apr 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/library_promise.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,4 +215,29 @@ mergeInto(LibraryManager.library, {
#endif
return id;
},

emscripten_promise_any__deps: ['$promiseMap', '$idsToPromises'],
emscripten_promise_any: function(idBuf, errorBuf, size) {
var promises = idsToPromises(idBuf, size);
#if RUNTIME_DEBUG
dbg('emscripten_promise_any: ' + promises);
#endif
#if ASSERTIONS
assert(typeof Promise.any !== 'undefined', "Promise.any does not exist");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do something more like this.. but its probably not worth it?:

#if ENV_MAY_BE_NODE
if (ENV_IS_NODE && nodeIsOld) {
  abort("helpful message")
}
#endif

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, just having this assertion seems more general. For example it would work in old browsers that don't support Promise.any as well. I'd be happy to make the assertion message more useful, but I think the current message at least delivers the most important piece of information.

#endif
var id = promiseMap.allocate({
promise: Promise.any(promises).catch((err) => {
if (errorBuf) {
for (var i = 0; i < size; i++) {
{{{ makeSetValue('errorBuf', `i*${POINTER_SIZE}`, 'err.errors[i]', '*') }}};
}
}
throw errorBuf;
})
});
#if RUNTIME_DEBUG
dbg('create: ' + id);
#endif
return id;
}
});
1 change: 1 addition & 0 deletions src/library_sigs.js
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,7 @@ sigs = {
emscripten_print_double__sig: 'idpi',
emscripten_promise_all__sig: 'pppp',
emscripten_promise_all_settled__sig: 'pppp',
emscripten_promise_any__sig: 'pppp',
emscripten_promise_create__sig: 'p',
emscripten_promise_destroy__sig: 'vp',
emscripten_promise_resolve__sig: 'vpip',
Expand Down
11 changes: 11 additions & 0 deletions system/include/emscripten/promise.h
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,17 @@ typedef struct em_settled_result_t {
__attribute__((warn_unused_result)) em_promise_t emscripten_promise_all_settled(
em_promise_t* promises, em_settled_result_t* results, size_t num_promises);

// Call Promise.any to create and return a new promise that is fulfilled once
// any of the `num_promises` input promises passed in `promises` has been
// fulfilled or is rejected once all of the input promises have been rejected.
// If the returned promise is fulfilled, it will be fulfilled with the same
// value as the first fulfilled input promise. Otherwise, if the returned
// promise is rejected, the rejection reasons for each input promise will be
// written to the `errors` buffer if it is non-null. The rejection reason for
// the returned promise will also be the address of the `errors` buffer.
__attribute__((warn_unused_result)) em_promise_t emscripten_promise_any(
em_promise_t* promises, void** errors, size_t num_promises);

#ifdef __cplusplus
}
#endif
126 changes: 123 additions & 3 deletions test/core/test_promise.c
Original file line number Diff line number Diff line change
Expand Up @@ -334,13 +334,131 @@ test_all_settled(void** result, void* data, void* value) {
emscripten_promise_destroy(null_in[2]);

em_promise_t to_finish[3] = {empty_checked, full_checked, null_checked};
em_promise_t finish_test_all = emscripten_promise_all(to_finish, NULL, 3);
em_promise_t finish_test_all_settled =
emscripten_promise_all(to_finish, NULL, 3);

emscripten_promise_destroy(empty_checked);
emscripten_promise_destroy(full_checked);
emscripten_promise_destroy(null_checked);

*result = finish_test_all;
*result = finish_test_all_settled;
return EM_PROMISE_MATCH_RELEASE;
}

typedef struct promise_any_state {
size_t size;
em_promise_t in[3];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are these 3 constants?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are the (up to) 3 input promises. They're not actually needed after the initial call to emscripten_promise_any, but it's convenient to bundle all the input, output, and expected state together in a single struct.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused, I don't see any mention of a limit of 3 on input promises in promise.h? The methods all take a num_promises without a mention of a limit in the comments.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, this is just used in this test file, which arbitrarily chooses to call the API with three promises.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh ok, sorry, that's what I was missing...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this C API is a little funky because 100% of the implementation is in JS. There's no .c implementation file for the API at all.

void* expected;
void* err[3];
void* expected_err[3];
} promise_any_state;

static em_promise_result_t
check_promise_any_result(void** result, void* data, void* value) {
promise_any_state* state = (promise_any_state*)data;
emscripten_console_logf("promise_any result: %ld", (uintptr_t)value);
assert(value == state->expected);
free(state);
return EM_PROMISE_FULFILL;
}

static em_promise_result_t
check_promise_any_err(void** result, void* data, void* value) {
promise_any_state* state = (promise_any_state*)data;
assert(value == state->err);
emscripten_console_log("promise_any reasons:");
for (size_t i = 0; i < state->size; ++i) {
emscripten_console_logf("%ld", (uintptr_t)state->err[i]);
assert(state->err[i] == state->expected_err[i]);
}
free(state);
return EM_PROMISE_FULFILL;
}

static em_promise_result_t test_any(void** result, void* data, void* value) {
emscripten_console_log("test_any");
assert(data == (void*)6);

// No input should be handled ok and be rejected.
promise_any_state* state = malloc(sizeof(promise_any_state));
*state = (promise_any_state){
.size = 0, .in = {}, .expected = NULL, .err = {}, .expected_err = {}};
em_promise_t empty =
emscripten_promise_any(state->in, state->err, state->size);
em_promise_t empty_checked =
emscripten_promise_then(empty, fail, check_promise_any_err, state);
emscripten_promise_destroy(empty);

// The first fulfilled promise should be propagated.
state = malloc(sizeof(promise_any_state));
*state = (promise_any_state){.size = 3,
.in = {emscripten_promise_create(),
emscripten_promise_create(),
emscripten_promise_create()},
.expected = (void*)42,
.err = {},
.expected_err = {}};
em_promise_t full =
emscripten_promise_any(state->in, state->err, state->size);
em_promise_t full_checked =
emscripten_promise_then(full, check_promise_any_result, fail, state);
emscripten_promise_destroy(full);
emscripten_promise_resolve(state->in[0], EM_PROMISE_REJECT, (void*)41);
emscripten_promise_resolve(state->in[1], EM_PROMISE_FULFILL, (void*)42);
emscripten_promise_resolve(state->in[2], EM_PROMISE_FULFILL, (void*)43);
emscripten_promise_destroy(state->in[0]);
emscripten_promise_destroy(state->in[1]);
emscripten_promise_destroy(state->in[2]);

// If all promises are rejected, the result will be rejected with the reasons.
state = malloc(sizeof(promise_any_state));
*state =
(promise_any_state){.size = 3,
.in = {emscripten_promise_create(),
emscripten_promise_create(),
emscripten_promise_create()},
.expected = NULL,
.err = {},
.expected_err = {(void*)42, (void*)43, (void*)44}};
em_promise_t rejected =
emscripten_promise_any(state->in, state->err, state->size);
em_promise_t rejected_checked =
emscripten_promise_then(rejected, fail, check_promise_any_err, state);
emscripten_promise_destroy(rejected);
emscripten_promise_resolve(state->in[0], EM_PROMISE_REJECT, (void*)42);
emscripten_promise_resolve(state->in[1], EM_PROMISE_REJECT, (void*)43);
emscripten_promise_resolve(state->in[2], EM_PROMISE_REJECT, (void*)44);
emscripten_promise_destroy(state->in[0]);
emscripten_promise_destroy(state->in[1]);
emscripten_promise_destroy(state->in[2]);

// Same, but now the error reason buffer is null.
em_promise_t null_in[3] = {
emscripten_promise_create(),
emscripten_promise_create(),
emscripten_promise_create(),
};
em_promise_t null = emscripten_promise_any(null_in, NULL, 3);
em_promise_t null_checked =
emscripten_promise_then(null, fail, check_null, NULL);
emscripten_promise_destroy(null);
emscripten_promise_resolve(null_in[0], EM_PROMISE_REJECT, (void*)42);
emscripten_promise_resolve(null_in[1], EM_PROMISE_REJECT, (void*)43);
emscripten_promise_resolve(null_in[2], EM_PROMISE_REJECT, (void*)44);
emscripten_promise_destroy(null_in[0]);
emscripten_promise_destroy(null_in[1]);
emscripten_promise_destroy(null_in[2]);

em_promise_t to_finish[4] = {
empty_checked, full_checked, rejected_checked, null_checked};
em_promise_t finish_test_any = emscripten_promise_all(to_finish, NULL, 4);

emscripten_promise_destroy(empty_checked);
emscripten_promise_destroy(full_checked);
emscripten_promise_destroy(rejected_checked);
emscripten_promise_destroy(null_checked);

*result = finish_test_any;
return EM_PROMISE_MATCH_RELEASE;
}

Expand Down Expand Up @@ -390,8 +508,9 @@ int main() {
em_promise_t test4 = emscripten_promise_then(test3, test_all, fail, (void*)4);
em_promise_t test5 =
emscripten_promise_then(test4, test_all_settled, fail, (void*)5);
em_promise_t test6 = emscripten_promise_then(test5, test_any, fail, (void*)6);
em_promise_t assert_stack =
emscripten_promise_then(test5, check_stack, fail, NULL);
emscripten_promise_then(test6, check_stack, fail, NULL);
em_promise_t end = emscripten_promise_then(assert_stack, finish, fail, NULL);

emscripten_promise_resolve(start, EM_PROMISE_FULFILL, NULL);
Expand All @@ -403,6 +522,7 @@ int main() {
emscripten_promise_destroy(test3);
emscripten_promise_destroy(test4);
emscripten_promise_destroy(test5);
emscripten_promise_destroy(test6);
emscripten_promise_destroy(assert_stack);
emscripten_promise_destroy(end);

Expand Down
7 changes: 7 additions & 0 deletions test/core/test_promise.out
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,11 @@ promise_all_settled results:
fulfill 42
reject 43
fulfill 44
test_any
promise_any reasons:
promise_any result: 42
promise_any reasons:
42
43
44
finish