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

Support callFunction API #850

Merged
merged 18 commits into from
Jun 5, 2020
Merged

Support callFunction API #850

merged 18 commits into from
Jun 5, 2020

Conversation

samouri
Copy link
Member

@samouri samouri commented Apr 23, 2020

summary
The ability to invoke arbitrary worker functions from the main thread and get the result would be useful to two AMP use-cases, Protocol Adapters and Allow real JS in amp-bind.

This PR exposes two new interfaces. A function named callFunction invoking functions on the worker, as well as a method for the worker code to mark which functions should be exposed to the main thread. I implement this by changing the return value from upgradeElement from a standard Worker, into a wrapper object. Notably, it still delegates the onerror and .terminate() function calls to the underlying worker.

// index.html
<div src="call-function.js" id="upgrade-me"></div>
<script type="module">
  import {upgradeElement} from '/dist/amp/main.mjs';
  const worker = await (upgradeElement(document.getElementById('upgrade-me'), '/dist/amp/worker/worker.mjs').)

   const result = await worker.callFunction('getRemoteData');
   console.log(result);
</script>

// worker.js
function getRemoteData() {
  return Promise.resolve({ big: 'tuna' });
}
exportFunction("getRemoteData", getRemoteData)

questions i'd like help with

  1. I noticed we don't specify filesize requirements for amp versions. Is this intentional / should I add them?

todo / next steps

  • (Future PR) create a "lite" build for those that don't need to interact with DOM.
  • support passing arguments
  • add tests

cc @choumx @jridgewell

@samouri samouri marked this pull request as ready for review April 24, 2020 18:10
Copy link
Collaborator

@dreamofabear dreamofabear left a comment

Choose a reason for hiding this comment

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

Bikeshed: "Call function"? :)

src/main-thread/worker.ts Outdated Show resolved Hide resolved
src/worker-thread/function.ts Outdated Show resolved Hide resolved
@samouri samouri changed the title Support invokeFunction API Support callFunction API Apr 28, 2020
@samouri
Copy link
Member Author

samouri commented May 5, 2020

To summarize some of the discussion from late last week with @choumx and @jridgewell, here are the next steps:

  1. Change the interface of worker-dom such that the API returns a new custom object with just callFunction and terminate as opposed to the standard Worker interface.
  2. Bump version
  3. TBD what to do about workers declaring exported functions.

src/main-thread/commands/function.ts Outdated Show resolved Hide resolved
package.json Outdated Show resolved Hide resolved

const index = fnCallCount;
promiseMap[index] = { resolve, reject, promise };
return { promise, index: fnCallCount++ };
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we need to worry about overflow?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think so. The fnCallCount is only used to correlate fn call requests and its associated response. Reusing an older index is fine as long as the reused index had actually completed.

It would technically be a bug if there were some super super long running task with index=0 and then while it were running, we launched Number.MAX_VALUE other calls until overflow. And then the next call would overwrite the first one.

Seems okay to me?

Copy link
Member Author

@samouri samouri May 19, 2020

Choose a reason for hiding this comment

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

TIL js doesn't overflow integers! It rounds to either Number.MAX_VALUE or to Infinity.

For example, Number.MAX_VALUE + 1 === Number.MAX_VALUE. For our use case I'd prefer it wrapped, but I also really dont see anyone hitting 9 quadrillion callFunction requests

Copy link
Collaborator

Choose a reason for hiding this comment

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

Me neither but should be easy to wrap to 0 when we hit MAX_VALUE.

Copy link
Member Author

Choose a reason for hiding this comment

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

sure

src/main-thread/commands/function.ts Outdated Show resolved Hide resolved
src/main-thread/exported-worker.ts Show resolved Hide resolved
src/main-thread/exported-worker.ts Outdated Show resolved Hide resolved
src/main-thread/configuration.ts Outdated Show resolved Hide resolved
@morsssss
Copy link
Contributor

Coolness

/cc @AndrewKGuan @alankent

if (status === ResolveOrReject.RESOLVE) {
promiseMap[index].resolve(JSON.parse(strings.get(value)));
} else {
promiseMap[index].reject(JSON.parse(strings.get(value)));
Copy link
Collaborator

Choose a reason for hiding this comment

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

How is undefined return value handled here? Are there other cases where JSON.parse can fail/throw?

Nit: Hoist the JSON.parse bit.

Copy link
Member Author

@samouri samouri May 27, 2020

Choose a reason for hiding this comment

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

Answer: it breaks :). Solving it required a bit of a hack since StringContext will return '' in the case of a falsy value. Therefore I added a new hasIndex method so we can distinguish between undefined and the empty string.

I've also added a test case for this.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Did you push changes yet?

Copy link
Member Author

@samouri samouri May 29, 2020

Choose a reason for hiding this comment

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

src/test/main-thread/commands/function.test.ts Outdated Show resolved Hide resolved
src/worker-thread/function.ts Show resolved Hide resolved
@samouri
Copy link
Member Author

samouri commented Jun 1, 2020

@choumx: can you merge? I don't have access

@kristoferbaxter
Copy link
Contributor

Added you to the write permission group @samouri.

worker
.callFunction("tooCoolToExist")
.catch((err) => console.error(err));
worker
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add an await example to this demo?

Copy link
Member Author

Choose a reason for hiding this comment

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

sure thing!

package.json Outdated
@@ -1,6 +1,6 @@
{
"name": "@ampproject/worker-dom",
"version": "0.24.0",
"version": "0.25.0",
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you need to manually update the version?

Copy link
Member Author

Choose a reason for hiding this comment

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

how can I automatically update the version?

Copy link
Contributor

Choose a reason for hiding this comment

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

The release script will do this automatically (check out RELEASING.md).

reject = rej;
});

// Wraparound to 0 in case someone attempts to register over 9 quadrillion promises.
Copy link
Contributor

Choose a reason for hiding this comment

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

LOL. This is likely not necessary, but funny nonetheless.

TransferrableMutationType.FUNCTION_CALL,
ResolveOrReject.REJECT,
index,
store(JSON.stringify(`[worker-dom]: Exported function "${fnIdentifier}" could not be found.`)),
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason we need to do this? Could we just reject with a generic error?

Copy link
Member Author

Choose a reason for hiding this comment

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

Isn't it more helpful to give the devs a useful error message?

Copy link
Member Author

Choose a reason for hiding this comment

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

Discussed this in Chat. The current method has higher DevEx whereas not having a helpful error message would save bytes.

We can have the best of both worlds by eventually having two builds, one for dev and one for prod. For now I'd like to merge with the slightly higher DevEx option esp as we work on getting to the optimal solution.

@samouri
Copy link
Member Author

samouri commented Jun 5, 2020

I'm going to hold off on merging this until I land a fix for #872.

Edit: nevermind. My gut says there is something wrong with my local setup since this built fine. Merging now

@samouri samouri merged commit 2bf0a5b into ampproject:master Jun 5, 2020
@samouri samouri deleted the exec-fn branch June 5, 2020 19:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants