Skip to main content

@traveloka/core/resource

This module is a utility to collect "resource query" from components, and fetch/resolve them from Content API.

Note that we have 3 kinds of resource query: "content resource (CR) query", "image resource (IR) query", and "image slider (IS) query". When we fetch CR and IR queries together, we call it a "batch resource query".

Important files

  • Fetchers

    • packages/core/resource/APIFetcher.ts — API fetchers for the usual case
    • packages/core/resource/APIFallbackFetcher.ts — API fetchers for fallback CR query (we fetch from AWS S3)
    • packages/core/resource/getResourceData.ts — Entrypoint for getStaticPage, getServerSideProps, and cacheableServerProps pages' data-fetcher to access CR/IR/IS APIs. (getInitialProps pages rely on resolveResource function of packages/core/resource/ResourceResolverAndCollector.ts instead)
  • Data structure

    • packages/core/resource/ResourceCache.ts — LRU store to store resource query result. The keys are in the format if name.entry
    • packages/core/resource/ResourceCollectorProxy.ts — Treat this to contain just the proxy objects with extra utilities. It includes utilities to "assign" resource query result and return it as a simple object.
  • Hooks

    • packages/core/resource/ResourceContext.tsx
    • packages/core/resource/ResourceContext.client.tsx
  • Utilities

    • packages/core/resource/packages/core/resource/ResourceResolverAndCollector.ts — Contains utilities for the first render pass of withTravelokaPage. (The resolveResource function renders the component and collects the query through side effect, while the collectXxxQuery functions which is called by the useXxxResource hooks collect the queries when the component is rendered.)

How this module works

GIP server-render flow

Refer to packages/core/next/withTravelokaPage.tsx.

During server prepass render

  1. Resource collector proxy is created
const resourceCollectorProxy = createResourceCollectorProxy();
  1. Page do an internal render pass with the resolveResource function.
    • The ResourceCollectorProxy passed inside sharedPageProps.resource is used to set the value of ResourceProvider during this render.
    • Inside useXxxResource hooks, if ResourceProvider holds the ResourceCollectorProxy proxy object (instead of the REAL resource data), it returns the result of collectContentQuery, which returns placeholder values (so that the component can have some data to render) AND ALSO modifies the internal resourceCollectorProxy.xxxQuery values to note which queries are used.
    • The resolveResource function fetches content API based on its internal resourceCollectorProxy.xxxQuery values.
    • The fetched value is stored in ResourceCollectorProxy via resourceCollectorProxy.assignResolvedXxxToCache functions, so that resourceCollectorProxy.getData() will return the actual resource data
const sharedPageProps = {
rawAppContext,
resource: resourceCollectorProxy
...pageProps,
};
resolveResource(
<DefaultRouterDataProvider ctx={ctx}>
<TravelokaApp Component={Page as any} pageProps={sharedPageProps} />
</DefaultRouterDataProvider>,
resourceProxy,
ctx
);
  1. The result is returned as part of the props
    • Note that resourceCollectorProxy.getData() depends on us calling resourceCollectorProxy.assignResolvedXxxToCache function first, so that it returns with the actual resource data.
return {
rawAppContext,
resource: resourceCollectorProxy.getData(),
...pageProps,
};

During server render

ResourceProvider holds a plain resource object.

  • Given useXxxResource(shape) hook call, the useXxxResource constructs a plain result object by inspecting the shape of shape and taking only the necessary values from ResourceCollectorProxy (the value that we fetched from server earlier). This result object is the resource data returned to the component.

GSP and GSSP server-render flow

Refer to packages/core/next/ssg/getSharedStaticProps.ts and packages/core/next/ssg/getSharedServerProps.ts.

This is simpler as we have listed all the listed resource needed when we call staticProps(...), or manually inside getSharedServerProps(ctx, ...resources)

  1. getSharedStaticProps and getSharedServerProps internally calls getResourceData(ctx, mergeResource(...resourceQuery)) so that it can return the resource object in the returned "shared props".

  2. getResourceData creates a collector function (this time, there is no prepass render as we should've listed all the required data), fetches the API, and assigns the data back to the collector object.

  3. getResourceData returns the resource data via collector.getData()

  4. The component renders with the actual resource data.

Client-render flow

Refer to packages/core/resource/ResourceContext.client.tsx.

Note that the value passed to ResourceProvider depends on the data-fetching function (see what object is passed to the resource shared props)

  • GIP (packages/core/next/withTravelokaPage.tsx): the value differs whether it's the render prepass (we pass ResourceCollectorProxy) or the normal render pass (we pass resourceCollectorProxy.getData())
  • GSP (packages/core/next/ssg/getSharedStaticProps.ts): we pass the resource cache object
  • GSSP (packages/core/next/ssg/getSharedServerProps.ts): we pass the resource cache object

Prepass (GIP page on client-navigation only)

sharedPageProps.resource initially contains proxy, so the following lines are triggered instead.

// During the first render (GIP page only), we use `ResourceCollectorProxy`
// Just collect the query to prevent request waterfall
if (collector !== null) {
return collectContentQuery(shape, collector);
}

It returns placeholder values (so that the component can have some data to render) AND ALSO modifies the internal resourceCollectorProxy.xxxQuery values to note which queries are used.

Normal pass

The following lines are triggered:

// For subsequent render, we return suspender to handle code-splitting
const fetchStatus = fetchXxxResource(shape, resourceCache.content);
return suspendsOrReturns(fetchStatus);

fetchXxxResource returns an object of { status, value }. The status could be "pending", "resolved", or "rejected". The value is a promise (if it is pending), the resource object (if it has resolved), or an error (if it rejects).

The internal working of fetchXxxResource is like this:

  • It checks if all the requested query has been resolved. If so, it resolves immediately with the resource data.
  • Otherwise, it checks if it has performed the same computation (query) before, and returns the result of that previous computation.
  • Otherwise, it fetches the API and returns a promise value (while asynchronously updating the cache object when the fetch has resolved)

Also note that due to the possibility of the suspendsOrReturns suspends if it encountered unresolved queries, components which might request additional queries in the client-side need to be wrapped in React <Suspense>, otherwise the page will break.