NullVoxPopuli / ember-resources

An implementation of Resources. Supports ember 3.28+

Home Page:https://github.com/NullVoxPopuli/ember-resources/blob/main/docs/docs/README.md

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Using Resources with Fastboot

roomman opened this issue · comments

Hi, I'm having a bit of a battle using ember-resources in a Fastboot app and am hoping to get some insights on what I'm doing wrong.

In the blog route of my app I want to make a fetch request to our CMS. I need that fetch to react when some state (the "audience" for example) changes, in order to personalise the page content.

To get content, I am currently using a Resource in the model hook, as follows:

import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';

import { useResource } from 'ember-resources';

import { FetchContent } from '../../resources/fetch-content';

export default class BlogIndexRoute extends Route {
  @service applicationState;

  model() {
    const query = `query blogPostCollection($limit: CollectionLimitInput!, $order: [BlogPostCollectionOrderOptionsInput]) {
        blogPostCollection(limit: $limit, order:$order) {
        total
        items {
          slug
          publishedAt
          title
          mainContent
          excerpt
          author {
            name
          }
        }
      }
    }`;

    return useResource(this, FetchContent, () => ({
      query: query,
      variables: {
        limit: 10,
        order: 'publishedAt_DESC',
      },
      audience: this.applicationState.audience,
      selector: 'blogPostCollection',
    }));
  }
}

The Resource implementation looks something like this:

import { tracked } from '@glimmer/tracking';
import { isDestroyed, isDestroying } from '@ember/destroyable';

import { encode } from 'base-64';
import { Resource } from 'ember-resources';
import fetch from 'fetch';

export class FetchContent extends Resource {
  @tracked isLoading = true;
  @tracked isError = false;
  @tracked content;

  constructor(owner, args, previous) {
    super(owner, args, previous);

    this.content = previous?.content;

    let { query, variables, audience, selector } = this.args.named;

    this.fetchFromLexas(query, variables, audience);
  }

  async fetchFromLexas(query, variables, audience) {
    // Define a JSON encoded LexasCMS Request Context
    let lexascmsRequestContext = JSON.stringify({
      audienceAttributes: {
        [audience]: true,
      },
    });
    // Base64 encode LexasCMS Request Context
    lexascmsRequestContext = encode(lexascmsRequestContext);

    try {
      const response = await fetch(
        'https://xxxxxxxxxxx.spaces.lexascms.com/delivery/graphql',
        {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            Accept: 'application/json',
            'x-lexascms-context': lexascmsRequestContext,
          },
          body: JSON.stringify({
            query,
            variables,
          }),
        }
      );
      const results = await response.json();
      if (isDestroyed(this) || isDestroying(this)) return;
      this.content = results.data;
      this.isLoading = false;
    } catch (err) {
      this.isError = true;
      console.log(`Lexas CMS Errors  - ${err}`);
    }
  }
}

This all works fine on the first render but whenever I refresh the blog page Fastboot raises this error: ReferenceError: document is not defined.

I've added a timeout to the Resource and that fixes things, so I know it's a timing issue. I'm just unsure how to refine this implementation to take account of Fastboot?

Does anyone have experience of using Resources with Fastboot, and would you be kind enough to share your insights? 🤞🏻

do you have a stack trace? or minimal reproduction?
I'm mostly wondering where document is being referenced

Thanks for the response @NullVoxPopuli.

I'm including a stack trace below but will follow up with a minimal reproduction:

ReferenceError: document is not defined
    at Function.__webpack_require__.l (/var/folders/cp/mp10cjhd1xdf8wg5gk7s9g500000gn/T/broccoli-49715MwN0ELyi3OtZ/out-236-packager_runner_embroider_webpack/assets/chunk.180905e8fe419f9ae7d1.js:175:28)
    at Object.__webpack_require__.f.j (/var/folders/cp/mp10cjhd1xdf8wg5gk7s9g500000gn/T/broccoli-49715MwN0ELyi3OtZ/out-236-packager_runner_embroider_webpack/assets/chunk.180905e8fe419f9ae7d1.js:281:37)
    at /var/folders/cp/mp10cjhd1xdf8wg5gk7s9g500000gn/T/broccoli-49715MwN0ELyi3OtZ/out-236-packager_runner_embroider_webpack/assets/chunk.180905e8fe419f9ae7d1.js:137:40
    at Array.reduce (<anonymous>)
    at Function.__webpack_require__.e (/var/folders/cp/mp10cjhd1xdf8wg5gk7s9g500000gn/T/broccoli-49715MwN0ELyi3OtZ/out-236-packager_runner_embroider_webpack/assets/chunk.180905e8fe419f9ae7d1.js:136:67)
    at Object.load (webpack://solomon-website/./assets/solomon-website.js?:107:60)
    at PrivateRouter.eval [as getRoute] (webpack://solomon-website/./node_modules/@embroider/router/index.js?:93:23)
    at UnresolvedRouteInfoByParam.fetchRoute (/var/folders/cp/mp10cjhd1xdf8wg5gk7s9g500000gn/T/broccoli-49715MwN0ELyi3OtZ/out-236-packager_runner_embroider_webpack/assets/router_js.js:847:1)
    at UnresolvedRouteInfoByParam.get route [as route] (/var/folders/cp/mp10cjhd1xdf8wg5gk7s9g500000gn/T/broccoli-49715MwN0ELyi3OtZ/out-236-packager_runner_embroider_webpack/assets/router_js.js:767:1)
    at URLTransitionIntent.applyToState (/var/folders/cp/mp10cjhd1xdf8wg5gk7s9g500000gn/T/broccoli-49715MwN0ELyi3OtZ/out-236-packager_runner_embroider_webpack/assets/router_js.js:1367:1)
    at PrivateRouter.getTransitionByIntent (/var/folders/cp/mp10cjhd1xdf8wg5gk7s9g500000gn/T/broccoli-49715MwN0ELyi3OtZ/out-236-packager_runner_embroider_webpack/assets/router_js.js:1514:1)
    at PrivateRouter.transitionByIntent (/var/folders/cp/mp10cjhd1xdf8wg5gk7s9g500000gn/T/broccoli-49715MwN0ELyi3OtZ/out-236-packager_runner_embroider_webpack/assets/router_js.js:1469:1)
    at PrivateRouter.doTransition (/var/folders/cp/mp10cjhd1xdf8wg5gk7s9g500000gn/T/broccoli-49715MwN0ELyi3OtZ/out-236-packager_runner_embroider_webpack/assets/router_js.js:1605:1)
    at PrivateRouter.handleURL (/var/folders/cp/mp10cjhd1xdf8wg5gk7s9g500000gn/T/broccoli-49715MwN0ELyi3OtZ/out-236-packager_runner_embroider_webpack/assets/router_js.js:2061:1)
    at Router._doURLTransition (/var/folders/cp/mp10cjhd1xdf8wg5gk7s9g500000gn/T/broccoli-49715MwN0ELyi3OtZ/out-236-packager_runner_embroider_webpack/assets/@ember/-internals/routing/lib/system/router.js:485:1)
    at Router.handleURL (/var/folders/cp/mp10cjhd1xdf8wg5gk7s9g500000gn/T/broccoli-49715MwN0ELyi3OtZ/out-236-packager_runner_embroider_webpack/assets/@ember/-internals/routing/lib/system/router.js:479:1)
    at Class.visit (/var/folders/cp/mp10cjhd1xdf8wg5gk7s9g500000gn/T/broccoli-49715MwN0ELyi3OtZ/out-236-packager_runner_embroider_webpack/assets/@ember/application/instance.js:250:1)
    at EmberApp._visit (/Users/simon/Sites/solomon-website/node_modules/fastboot/src/ember-app.js:265:20)
    at runMicrotasks (<anonymous>)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at EmberApp.visit (/Users/simon/Sites/solomon-website/node_modules/fastboot/src/ember-app.js:325:7)
    at FastBoot.visit (/Users/simon/Sites/solomon-website/node_modules/fastboot/src/index.js:86:18)
    at /Users/simon/Sites/solomon-website/node_modules/fastboot-express-middleware/src/index.js:33:20

When I debug into the stack, I can see document is called in one of the Webpack chunks that Fastboot's middleware is trying to import:

/******/ 	/* webpack/runtime/load script */
/******/ 	(() => {
/******/ 		var inProgress = {};
/******/ 		var dataWebpackPrefix = "solomon-website:";
/******/ 		// loadScript function to load a script via script tag
/******/ 		__webpack_require__.l = (url, done, key, chunkId) => {
/******/ 			if(inProgress[url]) { inProgress[url].push(done); return; }
/******/ 			var script, needAttach;
/******/ 			if(key !== undefined) {
/******/ 				var scripts = document.getElementsByTagName("script");
/******/ 				for(var i = 0; i < scripts.length; i++) {
/******/ 					var s = scripts[i];
/******/ 					if(s.getAttribute("src") == url || s.getAttribute("data-webpack") == dataWebpackPrefix + key) { script = s; break; }
/******/ 				}
/******/ 			}
/******/ 			if(!script) {
/******/ 				needAttach = true;
/******/ 				script = document.createElement('script');
/******/ 		
/******/ 				script.charset = 'utf-8';
/******/ 				script.timeout = 120;
/******/ 				if (__webpack_require__.nc) {
/******/ 					script.setAttribute("nonce", __webpack_require__.nc);
/******/ 				}
/******/ 				script.setAttribute("data-webpack", dataWebpackPrefix + key);
/******/ 				script.src = url;
/******/ 			}
/******/ 			inProgress[url] = [done];
/******/ 			var onScriptComplete = (prev, event) => {
/******/ 				// avoid mem leaks in IE.
/******/ 				script.onerror = script.onload = null;
/******/ 				clearTimeout(timeout);
/******/ 				var doneFns = inProgress[url];
/******/ 				delete inProgress[url];
/******/ 				script.parentNode && script.parentNode.removeChild(script);
/******/ 				doneFns && doneFns.forEach((fn) => (fn(event)));
/******/ 				if(prev) return prev(event);
/******/ 			}
/******/ 			;
/******/ 			var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);
/******/ 			script.onerror = onScriptComplete.bind(null, script.onerror);
/******/ 			script.onload = onScriptComplete.bind(null, script.onload);
/******/ 			needAttach && document.head.appendChild(script);
/******/ 		};
/******/ 	})();

I had hoped that be implementing useResource in the route's model hook, the rendering would be delayed until the fetch had resolved.

had hoped that be implementing useResource in the route's model hook, the rendering would be delayed until the fetch had resolved.

resources are explicitly non-blocking, non-async so that they can be used in derived data flows (like components with a bunch of getters, etc).

additionally, the resource itself, while it can be used anywhere -- it's best in the class body (for caching purposes). 🤔 I'm sure the README is lacking in this regard, but if you have any suggestions about how to make that more clear, that'd be a huge help!

You are in luck tho, because resources are "just classes with a convention", you can make your model hook await your request, even though you don't end up needing the useResource bit:

export default class BlogIndexRoute extends Route {
  @service applicationState;

  model() {
    const query = `query blogPostCollection($limit: CollectionLimitInput!, $order: [BlogPostCollectionOrderOptionsInput]) {
        blogPostCollection(limit: $limit, order:$order) {
          ✂️
        }
      }
    }`;

    let fetcher = new FetchContent(getOwner(this), { 
      named: {
        query: query,
        variables: {
          limit: 10,
          order: 'publishedAt_DESC',
        },
        audience: this.applicationState.audience,
        selector: 'blogPostCollection',
      });
      
      await // (something ||  there is no current reference to the promise returned
      //  from the function invocation, so you'd need to put that on a property or something)
      
      return fetcher;
  }
}

Hope this helps!

This is really useful, many thanks. I actually started out with the Resource implemented in the controller and not the route but I was still getting the error from Fastboot on the second render. I refactored into the route in the hope that I could leverage deferred rendering, which it looks like your suggestion will allow.

As a general rule, though, how do you foresee Resources being used alongside SSR if they are best in the class body (by which I assume you mean "ideally, using putting them in a route")?

I think it would be great to add something to the docs (which I'm happy to help with) to describe strategies for implementing Resources in a way that is most compatible with Fastboot?

but I was still getting the error from Fastboot on the second render.

It seems like the error is coming from a chunk loader, which is maybe from webpack and has nothing (directly) to do with resources (as they do nothing with the document/window/etc) -- sorry I didn't catch that earlier (or if I did, I thought going back to the old loading technique would defer rendering of things that needed the chunked files until after your app was rendered for real -- does make me wonder though if fastboot needs to support document/window/etc -- sure seems like a lot of footguns around not having those... Maybe I'll look in to that at some point... anyway 🙃 ).

Seems like this is your issue? embroider-build/embroider#664 (though stack trace is slightly different -- and I couldn't find a similar issue on ember-auto-import).

cc @ef4 and @realate (the only folks I know that are working on fastboot/auto-import/embroider interop)

Using ember-auto-import in fastboot, we're supposed to never go down the code path that tries to do chunk loading because we're supposed to have pre-loaded all the chunks. If you can find a way to reproduce this crash please provide instructions in an ember-auto-import issue.

(I'm assuming this is ember-auto-import and not embroider but if you're using embroider, file it there instead, the issue would be similar.)

@NullVoxPopuli I'm really sorry for the red herring here, it clearly has nothing to do with ember-resources. Apologies for wasting your time, it's just a coincidence that adding a timeout into the Resource fixed it.

Thanks @ef4 for the guidance. I'm pretty sure it's an Embroider issue and have a reproduction. I will create an Issue today.

Best to both of you!

@roomman no worries! I've made sillier mistakes, it's totally understandable.
It's never a waste of time, because the whole ecosystem needs help at one point or another. Also, it means we can improve upon our errors. ❤️

For future travellers, I've added the issue, here: embroider-build/embroider#1060