Skip to content

Commit

Permalink
feat: prefetch Link q-data and imports (QwikDev#1178)
Browse files Browse the repository at this point in the history
  • Loading branch information
adamdbradley authored Aug 30, 2022
1 parent aadc5da commit f1cc869
Show file tree
Hide file tree
Showing 17 changed files with 84 additions and 64 deletions.
6 changes: 0 additions & 6 deletions packages/qwik-city/buildtime/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export function createBuildContext(
target?: 'ssr' | 'client'
) {
const ctx: BuildContext = {
buildId: generateBuildId(),
rootDir: normalizePath(rootDir),
opts: normalizeOptions(rootDir, userOpts),
routes: [],
Expand All @@ -29,7 +28,6 @@ export function createBuildContext(

export function resetBuildContext(ctx: BuildContext | null) {
if (ctx) {
ctx.buildId = generateBuildId();
ctx.routes.length = 0;
ctx.errors.length = 0;
ctx.layouts.length = 0;
Expand Down Expand Up @@ -74,7 +72,3 @@ function normalizeOptions(rootDir: string, userOpts: PluginOptions | undefined)

return opts;
}

function generateBuildId() {
return Math.random().toString(36).slice(2);
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,5 @@ export function generateQwikCityPlan(ctx: BuildContext) {

c.push(`export const cacheModules = ${JSON.stringify(!ctx.isDevServer)};`);

c.push(`export const buildId = ${JSON.stringify(ctx.buildId)};`);

return esmImports.join('\n') + c.join('\n');
}
1 change: 0 additions & 1 deletion packages/qwik-city/buildtime/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export interface BuildContext {
buildId: string;
rootDir: string;
opts: NormalizedPluginOptions;
routes: BuildRoute[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export function pageHandler<T = any>(

if (typeof stream.clientData === 'function') {
// a data fn was provided by the request context
// useful for writing qdata.json during SSG
// useful for writing q-data.json during SSG
stream.clientData(await getClientPageData(userResponse, result));
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ export function updateRequestCtx(requestCtx: QwikCityRequestContext, trailingSla
}
}

const QDATA_JSON = '/qdata.json';
const QDATA_JSON = '/q-data.json';
const QDATA_JSON_LEN = QDATA_JSON.length;

const ABORT_INDEX = 999999999;
Original file line number Diff line number Diff line change
Expand Up @@ -243,28 +243,28 @@ test('add trailing slash, PageModule', async () => {
});

test('updateRequestCtx, trailing slash', () => {
const requestCtx = mockRequestContext({ url: '/about/qdata.json' });
const requestCtx = mockRequestContext({ url: '/about/q-data.json' });
updateRequestCtx(requestCtx, true);
equal(requestCtx.url.pathname, '/about/');
equal(requestCtx.request.headers.get('Accept'), 'application/json');
});

test('updateRequestCtx, no trailing slash', () => {
const requestCtx = mockRequestContext({ url: '/about/qdata.json' });
const requestCtx = mockRequestContext({ url: '/about/q-data.json' });
updateRequestCtx(requestCtx, false);
equal(requestCtx.url.pathname, '/about');
equal(requestCtx.request.headers.get('Accept'), 'application/json');
});

test('updateRequestCtx, root, trailing slash', () => {
const requestCtx = mockRequestContext({ url: '/qdata.json' });
const requestCtx = mockRequestContext({ url: '/q-data.json' });
updateRequestCtx(requestCtx, true);
equal(requestCtx.url.pathname, '/');
equal(requestCtx.request.headers.get('Accept'), 'application/json');
});

test('updateRequestCtx, root, no trailing slash', () => {
const requestCtx = mockRequestContext({ url: '/qdata.json' });
const requestCtx = mockRequestContext({ url: '/q-data.json' });
updateRequestCtx(requestCtx, false);
equal(requestCtx.url.pathname, '/');
equal(requestCtx.request.headers.get('Accept'), 'application/json');
Expand Down
4 changes: 2 additions & 2 deletions packages/qwik-city/runtime/src/app/tests/api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ test('Page route, accept application/javascript', async ({ page: api }) => {
expect(clientData.prefetch).toBeDefined();
});

test('Page qdata.json route', async ({ page: api }) => {
const rsp = (await api.goto('/products/hat/qdata.json'))!;
test('Page q-data.json route', async ({ page: api }) => {
const rsp = (await api.goto('/products/hat/q-data.json'))!;
expect(rsp.status()).toBe(200);
expect(rsp.headers()['content-type']).toBe('application/json; charset=utf-8');

Expand Down
4 changes: 2 additions & 2 deletions packages/qwik-city/runtime/src/library/client-navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ export const getClientNavPath = (props: Record<string, any>, baseUrl: { href: st
return null;
};

export const getClientEndpointPath = (pagePathname: string, buildId: string) =>
pagePathname + (pagePathname.endsWith('/') ? '' : '/') + 'qdata.json?v=' + buildId;
export const getClientEndpointPath = (pagePathname: string) =>
pagePathname + (pagePathname.endsWith('/') ? '' : '/') + 'q-data.json';

const handleScroll = async (win: Window, previousUrl: SimpleURL, newUrl: SimpleURL) => {
const doc = win.document;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,12 +231,12 @@ const baseUrl = new URL('https://qwik.dev/');
});

[
{ pathname: '/', expect: '/qdata.json?v=abc' },
{ pathname: '/about', expect: '/about/qdata.json?v=abc' },
{ pathname: '/about/', expect: '/about/qdata.json?v=abc' },
{ pathname: '/', expect: '/q-data.json' },
{ pathname: '/about', expect: '/about/q-data.json' },
{ pathname: '/about/', expect: '/about/q-data.json' },
].forEach((t) => {
test(`getClientEndpointUrl("${t.pathname}")`, () => {
const url = getClientEndpointPath(t.pathname, 'abc');
const url = getClientEndpointPath(t.pathname);
equal(url, t.expect);
});
});
Expand Down
18 changes: 11 additions & 7 deletions packages/qwik-city/runtime/src/library/link-component.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { component$, Slot, QwikIntrinsicElements } from '@builder.io/qwik';
import { getClientNavPath } from './client-navigation';
import type { QrlPrefetchData } from './service-worker/types';
import type { QPrefetchData } from './service-worker/types';
import { fetchClientData } from './use-endpoint';
import { useLocation, useNavigate } from './use-functions';

/**
Expand All @@ -23,18 +24,21 @@ export const Link = component$<LinkProps>((props) => {
nav.path = linkProps.href!;
}
}}
onMouseOver$={() => {
if (clientNavPath) {
const data: QrlPrefetchData = { links: [clientNavPath] };
dispatchEvent(new CustomEvent('qprefetch', { detail: data }));
}
}}
onMouseOver$={() => prefetchLinkResources(clientNavPath)}
>
<Slot />
</a>
);
});

export const prefetchLinkResources = (clientNavPath: string | null) => {
if (clientNavPath) {
fetchClientData(clientNavPath);
const data: QPrefetchData = { links: [clientNavPath] };
dispatchEvent(new CustomEvent('qprefetch', { detail: data }));
}
};

type AnchorAttributes = QwikIntrinsicElements['a'];

/**
Expand Down
1 change: 0 additions & 1 deletion packages/qwik-city/runtime/src/library/qwik-city-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,3 @@ export const menus: MenuData[] = [];
export const trailingSlash = false;
export const basePathname = '/';
export const cacheModules = false;
export const buildId = 'qwik';
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,12 @@ export const prefetchBundleNames = (
baseUrl: URL,
prefetchBundles: string[]
) => {
const fetches: Promise<Response>[] = [];

const prefetchBundle = (bundleName: string) => {
try {
const url = new URL(bundleName, baseUrl).href;
if (!existingPrefetches.has(url)) {
existingPrefetches.add(url);
fetches.push(cachedFetch(qBuildCache, fetch, awaitingRequests, new Request(url)));
cachedFetch(qBuildCache, fetch, awaitingRequests, new Request(url));
}
} catch (e) {
console.error(e);
Expand All @@ -29,8 +27,6 @@ export const prefetchBundleNames = (
bundles[prefetchBundleName].forEach(prefetchBundle);
}
}

return Promise.all(fetches);
};

export const prefetchLinks = (
Expand All @@ -44,10 +40,10 @@ export const prefetchLinks = (
) => {
for (const linkPathname of prefetchLinkPathnames) {
for (const link of links) {
const pattern = link[0];
if (pattern.test(linkPathname)) {
const prefetchBundles = [...link[1], ...libraryBundles];
return prefetchBundleNames(bundles, qBuildCache, fetch, baseUrl, prefetchBundles);
if (link[0].test(linkPathname)) {
// prefetch bundles known for this route
prefetchBundleNames(bundles, qBuildCache, fetch, baseUrl, [...link[1], ...libraryBundles]);
break;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
export interface QrlPrefetchData {
export interface QPrefetchData {
urls?: string[];
links?: string[];
}

export interface QrlPrefetchMessage extends QrlPrefetchData {
export interface QPrefetchMessage extends QPrefetchData {
type: 'qprefetch';
base: string;
}

export type ServiceWorkerMessage = QrlPrefetchMessage;
export type ServiceWorkerMessage = QPrefetchMessage;

export interface ServiceWorkerMessageEvent {
data: ServiceWorkerMessage;
Expand Down
8 changes: 4 additions & 4 deletions packages/qwik-city/runtime/src/library/sw-register.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/* eslint-disable */
import type { QrlPrefetchData, QrlPrefetchMessage } from './service-worker/types';
import type { QPrefetchData, QPrefetchMessage } from './service-worker/types';

// Source for what becomes innerHTML to the <ServiceWorkerRegister/> script

((
queuedUrls: string[],
swReg?: QwikServiceWorkerRegistration,
sendPrefetch?: (data: QrlPrefetchData, qBase?: Element) => void,
sendPrefetch?: (data: QPrefetchData, qBase?: Element) => void,
initServiceWorker?: () => void
) => {
sendPrefetch = (data, qBase) => {
Expand Down Expand Up @@ -52,15 +52,15 @@ import type { QrlPrefetchData, QrlPrefetchMessage } from './service-worker/types
})([]);

interface QwikServiceWorker extends ServiceWorker {
postMessage(data: QrlPrefetchMessage): void;
postMessage(data: QPrefetchMessage): void;
}

interface QwikServiceWorkerRegistration extends ServiceWorkerRegistration {
active: QwikServiceWorker | null;
}

interface QPrefetchEvent extends CustomEvent {
detail: QrlPrefetchData;
detail: QPrefetchData;
}

declare const addEventListener: (type: 'qprefetch', cb: (ev: QPrefetchEvent) => void) => void;
61 changes: 46 additions & 15 deletions packages/qwik-city/runtime/src/library/use-endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useLocation, useQwikCityEnv } from './use-functions';
import { isServer } from '@builder.io/qwik/build';
import type { ClientPageData, GetEndpointData } from './types';
import { getClientEndpointPath } from './client-navigation';
import { buildId } from '@qwik-city-plan';
import type { QPrefetchData } from './service-worker/types';

/**
* @alpha
Expand All @@ -12,7 +12,7 @@ export const useEndpoint = <T = unknown>() => {
const loc = useLocation();
const env = useQwikCityEnv();

return useResource$<GetEndpointData<T>>(async ({ track, cleanup }) => {
return useResource$<GetEndpointData<T>>(({ track }) => {
const pathname = track(loc, 'pathname');

if (isServer) {
Expand All @@ -22,22 +22,53 @@ export const useEndpoint = <T = unknown>() => {
return env.response.body;
} else {
// fetch() for new data when the pathname has changed
const controller = typeof AbortController === 'function' ? new AbortController() : undefined;
cleanup(() => controller && controller.abort());
return fetchClientData(pathname);
}
});
};

const endpointUrl = getClientEndpointPath(pathname, buildId);
const clientResponse = await fetch(endpointUrl, {
signal: controller && controller.signal,
});
const cachedClientDataResponses: { c: Promise<ClientPageData>; t: number; u: string }[] = [];

const contentType = clientResponse.headers.get('content-type') || '';
export const fetchClientData = async (pathname: string) => {
const endpointUrl = getClientEndpointPath(pathname);
const i = cachedClientDataResponses.findIndex((cached) => cached.u === endpointUrl);
const now = Date.now();
let cachedClientDataResponse = cachedClientDataResponses[i];

if (clientResponse.ok && contentType.includes('json')) {
const clientData: ClientPageData = await clientResponse.json();
return clientData.data as T;
if (!cachedClientDataResponse || cachedClientDataResponse.t + 300000 < now) {
cachedClientDataResponse = {
c: new Promise<ClientPageData>((resolve, reject) => {
fetch(endpointUrl).then((clientResponse) => {
try {
const contentType = clientResponse.headers.get('content-type') || '';
if (clientResponse.ok && contentType.includes('json')) {
clientResponse.json().then((clientData: ClientPageData) => {
const prefetchData: QPrefetchData = { links: clientData.prefetch };
dispatchEvent(new CustomEvent('qprefetch', { detail: prefetchData }));
resolve(clientData);
}, reject);
} else {
reject(`Invalid endpoint response: ${clientResponse.status}, ${contentType}`);
}
} catch (e) {
reject(e);
}
}, reject);
}),
t: now,
u: endpointUrl,
};
if (i > -1) {
cachedClientDataResponses[i] = cachedClientDataResponse;
} else {
cachedClientDataResponses.push(cachedClientDataResponse);
if (cachedClientDataResponses.length > 30) {
cachedClientDataResponses.splice(0, cachedClientDataResponses.length - 30);
}

throw new Error(`Invalid endpoint response: ${clientResponse.status}, ${contentType}`);
}
});
}

const clientPageData = await cachedClientDataResponse.c;

return clientPageData.data;
};
2 changes: 1 addition & 1 deletion packages/qwik-city/static/node/node-system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export async function createNodeSystem(opts: NodeStaticGeneratorOptions) {
if (!pathname.endsWith('/')) {
pathname += '/';
}
pathname += 'qdata.json';
pathname += 'q-data.json';
return join(outDir, pathname);
};

Expand Down
1 change: 0 additions & 1 deletion scripts/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,6 @@ declare module '@qwik-city-plan' {
export const trailingSlash: boolean;
export const basePathname: string;
export const cacheModules: boolean;
export const buildId: string;
}
`;
const srcModulesPath = join(config.packagesDir, 'qwik-city', 'lib');
Expand Down

0 comments on commit f1cc869

Please sign in to comment.