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

I2I: "protocol adapters" for client side data massaging. #26474

Closed
samouri opened this issue Jan 24, 2020 · 14 comments
Closed

I2I: "protocol adapters" for client side data massaging. #26474

samouri opened this issue Jan 24, 2020 · 14 comments
Assignees
Labels
INTENT TO IMPLEMENT Proposes implementation of a significant new feature. https://bit.ly/amp-contribute-code P1: High Priority WG: runtime

Comments

@samouri
Copy link
Member

samouri commented Jan 24, 2020

summary
The objective is to solve the protocol adapters use case. We want to allow developers to perform arbitrary client side transformations to an api response before handing it to an amp component, essentially trading UX for DX.

Since amp-script is already capable of performing xhr requests, data manipulations, and can call AMP.setState, it is close to being fully capable of servicing this use-case.

assumptions

  1. I2I: Allow amp-list to render from amp-state initially #26473 lands
  2. We are willing to make initialization from amp-state wait a window of time for data to appear.

basic amp-list w/ refresh example

<html>
  <head>...</head>
  <body>
    <amp-script script="animals-script">
      <button id="refresh-button"> Refresh data </button>
      <amp-list src="amp-state:animals">...</amp-list>
    </amp-script>
    <script type="text/plain" target="amp-script" id="animals-script">
       function fetchAnimalData() {
         fetch('url-with-data.json')
           .then(res => res.json())
           .then(transform)
           .then(array => AMP.setState({ animals: { items: array } })); 
       }

       document.getElementById('refresh-button').addEventListener('click', fetchAnimalData);
       fetchAnimalData(); // first load
    </script>
  </body>
</html>

motivation

  • Many frontend developers do not have control over their backend APIs. In order to play nicely with amp-list, they create proxy servers. This is a poor DX for something that could easily be solved via a sprinkling of JS.
  • It is currently impossible to take advantage of alternative data sources, i.e. websockets or rtc data connections.

caveats

  • amp-script has a nontrivial initialization cost. If after we perform measurements we deem it too high to release this feature, there are opportunities we can pursue for speeding up init, especially for this use case.
    • inlining the worker js into a string variable.
    • creating a lightweight version of worker-dom (without dom access, but with the ability to perform xhr and call AMP.setState).
  • We'll need further work to allow for reset-on-refresh behavior (solvable by exposing amp actions within amp-script).
@samouri samouri added the INTENT TO IMPLEMENT Proposes implementation of a significant new feature. https://bit.ly/amp-contribute-code label Jan 24, 2020
@samouri
Copy link
Member Author

samouri commented Jan 30, 2020

There is one thing about this proposal thats been bugging me. The inversion of control from amp-list to amp-script means that much of the edge-case handling inside of amp-list has to be recreated in userland.

Edge cases:

  1. When hitting a refresh button, they'd need to call an action we expose on amp-list in order to trigger reset-on-refresh
  2. infinite scrolling support would require the invention of convoluted new APIs to support.
  3. Error cases? In the case of a failed fetch, how should the amp-script communicate that to the amp-list?

We might be better served by allowing the src of amp-list to reference an amp-script function. That way amp-list still gets to decide when to call the fetch and what to do about the result.

For the same use case above:

<html>
  <head>...</head>
  <body>
    <button on="click:amp-list.refresh()"> Refresh data </button>
    <amp-list src="fn:animals-script.fetchAnimalData">...</amp-list>
    <script type="text/plain" target="amp-script" id="animals-script">
       function fetchAnimalData() {
         return fetch('url-with-data.json')
           .then(res => res.json())
           .then(data => { 
             let next = data.next ? `http://url-with-data-${data.next}` : undefined;
             return { items: data.animals, next };
           });
       }
    </script>
  </body>
</html>

Note: no use case should ever need to pass data into these functions since they can call AMP.getState().

Also note: this solution for protocol adapters would potentially create a clean solution for #25684

@morsssss
Copy link
Contributor

morsssss commented Feb 3, 2020

Your second example is what I was personally thinking about and hoping for:

<amp-list src="fn:animals-script.fetchAnimalData">

Generally speaking, I've been hoping that <amp-script> or a similar mechanism could simply host a collection of helper methods for both <amp-list> and <amp-bind>. I didn't even imagine that the components would need to be surrounded by <amp-script> - simply that we'd have a repository or class of these helper methods, living in a Worker.

So, devs would get to use all the features of <amp-list>, but just deploy a little JS to help them out.

If this works, you could get closer to my dream for #25684 - as you have pointed out!

/cc @alankent for his opinion

@morsssss
Copy link
Contributor

morsssss commented Feb 3, 2020

In other words, we keep the simplicity of our interactive components, but we let developers inject a little logic of their own in nice, native JavaScript.

@samouri
Copy link
Member Author

samouri commented Feb 3, 2020

primary question for design review

  • Should amp components or amp-script have control over the flow. What do web developers want more?
    • Giving amp components control means all of their edge cases / extended options are usable for this use case and amp-script is used merely as a data source.
    • Giving amp-script full control means we may need to expose more of the internals of components to be accessible by amp-script. Depending on their use case, developers would need to create their own success/error handlers, pagination, inf load, etc.

@alankent
Copy link

alankent commented Feb 4, 2020

I am not sure I follow "should AMP components or amp-script have control of the flow?" So trying to rephrase my thoughts.

I think what Ben said expresses my thoughts too. If I need to use amp-script to massage the request and response, I don't want to do a major page redesign thinking about different control flows. AMP pages for me should be declarative (as much as possible anyway). So just because I need a protocol adapter, I should not start needing amp-script wrappers around parts of the page (if possible).

I don't understand all the refresh etc edge conditions. But I think about it more simply. Consider a product page that has 2 selectors for shirt size and color. Updating either one should cause the page to refresh. I imagine AMP state would be used to remember the current values. Clicking a button should update the AMP state to the new selection. If there was no protocol adapter, that would trigger an API call and page render. That is the goal - update state and amp-bind causes the amp-list to refresh.

The only extra part to the mix is to say that the update to amp-state instead of causing amp-list to go fetch a [src]=... API call, instead amp-script code will be run that does the XHR call. The JavaScript also converts the response into JSON. So I provide a little bit of logic that fits into the current normal page flow.

So reloading a page should add like what a page would do without amp-script (just using amp-list and [src]=. As soon as the code on an AMP page has to change too much, I think things might be heading the wrong direction. (It might be unavoidable, but keep thinking declarative - AMP state remembers the current state. You make changes to the state to trigger other side effects, like the DOM updating or API calls being made.)

@mdmower
Copy link
Contributor

mdmower commented Feb 4, 2020

  • Should amp components or amp-script have control over the flow. What do web developers want more?

If I understand the question correctly, giving amp-script control over the page (AMP.setState) while in the middle of an amp-list data fetch sounds like a rats nest of race conditions. I'm strongly in favor of the simplest black box approach possible for this I2I. The data massage amp-script should have the ability to output JSON and otherwise have no ability to interact with the AMP document. I think the only features worth retaining in amp-script are the ability to inline a script and the script integrity validation (hash check).

@samouri
Copy link
Member Author

samouri commented Feb 4, 2020

@alankent, I'll write out the shirts example (reminds me of ampcamp!) through the lens of the two proposals.

(1) amp-script w/ most of the control

<html>
  <head>...</head>
  <body>
    <amp-script script="store-script">
      <button id="shirt-size-button"> rotate shirt size</button>
      <button id="shirt-color-button"> rotate shirt color </button>
      <amp-list src="amp-state:shirtData" [src]="shirtData">...</amp-list>
    </amp-script>
    <script type="text/plain" target="amp-script" id="store-script">
       function fetchShirtData() {
         const size = AMP.getState().selectedSize;
         const color = AMP.getState().selectedColor;
         fetch(`/api?size=${size}&color=${color}`)
           .then(res => res.json())
           .then(transform)
           .then(data => AMP.setState({ shirtData: data })); 
       }

       document.getElementById('shirt-size-button').addEventListener('click', () => {
         AMP.setState({selectedSize: ... }).then(fetchShirtData); 
       });
       document.getElementById('shirt-color-button').addEventListener('click', () => {
         AMP.setState({selectedColor: ... }).then(fetchShirtData); 
       });
       fetchShirtData(); // first load
    </script>
  </body>
</html>

(2) amp-list w/ most of the control

<html>
  <head>...</head>
  <body>
    <button id="shirt-size-button" on="tap:AMP.setState(...),items.refresh()"> rotate shirt size</button>
    <button id="shirt-color-button" on="tap:AMP.setState(...),items.refresh()"> rotate shirt color </button>
    <amp-list id="items" src="fn:store-script.fetchShirtData">...</amp-list>
    
    <amp-script script="store-script"></amp-script>
    <script type="text/plain" target="amp-script" id="store-script">
       function fetchShirtData() {
         const size = AMP.getState().selectedSize;
         const color = AMP.getState().selectedColor;
         return fetch(`/api?size=${size}&color=${color}`)
           .then(res => res.json())
           .then(transform)
       }
    </script>
  </body>
</html>

My take is that something more like (2) is significantly more preferable.

@morsssss
Copy link
Contributor

morsssss commented Feb 4, 2020

I agree that (2) is more preferable... preferabler?

I would even go further. I imagined that AMP would perform the fetch() as usual, simply passing along the raw data to the user JavaScript. I realize this is something different than what you're imagining. Something like this:

<amp-list id="items" src="'/api/fetchShirtData?size=' + products.size + '&color=' + products.color" fn="store-script"> ... </amp-list>

<amp-script script="store-script"></amp-script>
<script type="text/plain" target="amp-script" id="store-script">
    function massageShirtData(rawData) {
         let transformedData = ... ;
         return transformedData;
    }
</script>

What do you think?

@samouri
Copy link
Member Author

samouri commented Feb 4, 2020

@morsssss: I'm okay with that idea, it definitely simplifies the 80% use-case. My reservations would be:

  1. @mdmower has requested the ability to modify requests before they happen (i.e. adding a header or special encoding a parameter)
  2. (less important): this would close off the possibility of using alternative data sources. e.g. websockets or even random js logic (without a network request).

@alankent
Copy link

alankent commented Feb 4, 2020

Hi @samouri - no detailed comment from me just now, but I think you are understanding the issues correctly and heading in the right direction. I definitely want to add special headers etc (e.g. generate a SOAP request with special authentication headers, or generate 3 separate API requests and merge the results).

@mdmower
Copy link
Contributor

mdmower commented Feb 4, 2020

  1. @mdmower has requested the ability to modify requests before they happen (i.e. adding a header or special encoding a parameter)

I can't recall my requests exactly, but I suppose these suggestions depend on whether AMP creates the xhr or the worker. If the worker performs the xhr, you can leave it up to the publisher to encode url params as necessary and specify headers (notably, the accept header). If AMP performs the xhr, then yes, at a minimum, this feature should allow modifying the accept header.

@samouri
Copy link
Member Author

samouri commented Feb 5, 2020

The consensus in the design review was to pursue option (2) instead of option (1).

@morsssss
Copy link
Contributor

morsssss commented Feb 6, 2020

hooray!

(@alankent , I'm now getting hives at the memory of making SOAP services)

@samouri
Copy link
Member Author

samouri commented Aug 21, 2020

Closing as this feature has launched in #29689

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
INTENT TO IMPLEMENT Proposes implementation of a significant new feature. https://bit.ly/amp-contribute-code P1: High Priority WG: runtime
Projects
None yet
Development

No branches or pull requests

5 participants