diff --git a/dotcom-rendering/src/client/islands/getPriority.ts b/dotcom-rendering/src/client/islands/getPriority.ts new file mode 100644 index 00000000000..7fd2748f9b1 --- /dev/null +++ b/dotcom-rendering/src/client/islands/getPriority.ts @@ -0,0 +1,25 @@ +import type { Priority } from '../../lib/scheduler'; +import { isValidSchedulerPriority } from '../../lib/scheduler'; + +/** + * getPriority takes the given html element and returns its priority attribute + * + * We expect the element to always be a `gu-*` custom element + * + * @param marker : The html element that we want to read the priority attribute from; + * @returns + */ +export const getPriority = (marker: HTMLElement): Priority | undefined => { + const priority = marker.getAttribute('priority'); + + if (isValidSchedulerPriority(priority)) { + return priority; + } + + console.error( + 'Unable to find valid priority attribute on gu-island', + marker, + ); + + return; +}; diff --git a/dotcom-rendering/src/client/islands/initHydration.ts b/dotcom-rendering/src/client/islands/initHydration.ts index b8b2bdbec25..196a3e40b98 100644 --- a/dotcom-rendering/src/client/islands/initHydration.ts +++ b/dotcom-rendering/src/client/islands/initHydration.ts @@ -1,7 +1,10 @@ import type { EmotionCache } from '@emotion/cache'; +import { isUndefined } from '@guardian/libs'; +import { schedule } from '../../lib/scheduler'; import { doHydration } from './doHydration'; import { getConfig } from './getConfig'; import { getName } from './getName'; +import { getPriority } from './getPriority'; import { getProps } from './getProps'; import { onInteraction } from './onInteraction'; import { onNavigation } from './onNavigation'; @@ -38,51 +41,41 @@ export const initHydration = async ( const name = getName(element); const props = getProps(element); const config = getConfig(element); + const priority = getPriority(element); if (!name) return; + if (isUndefined(priority)) return; + + const scheduleHydration = () => + schedule( + name, + () => doHydration(name, props, element, emotionCache, config), + { priority }, + ); const deferUntil = element.getAttribute('deferuntil'); switch (deferUntil) { case 'idle': { - whenIdle(() => { - void doHydration(name, props, element, emotionCache, config); - }); + whenIdle(() => void scheduleHydration()); return; } case 'visible': { const rootMargin = element.getAttribute('rootmargin') ?? undefined; - whenVisible( - element, - () => { - void doHydration( - name, - props, - element, - emotionCache, - config, - ); - }, - { rootMargin }, - ); + whenVisible(element, () => void scheduleHydration(), { + rootMargin, + }); return; } case 'interaction': { - onInteraction(element, (targetElement) => { - void doHydration( - name, - props, - element, - emotionCache, - config, - ).then(() => { - targetElement.dispatchEvent(new MouseEvent('click')); - }); + onInteraction(element, async (targetElement) => { + await scheduleHydration(); + targetElement.dispatchEvent(new MouseEvent('click')); }); return; } case 'hash': { if (window.location.hash.includes(name) || hasLightboxHash(name)) { - void doHydration(name, props, element, emotionCache, config); + return scheduleHydration(); } else { // If we didn't find a matching hash on page load, set a // listener so that we check again each time the reader @@ -92,20 +85,14 @@ export const initHydration = async ( window.location.hash.includes(name) || hasLightboxHash(name) ) { - void doHydration( - name, - props, - element, - emotionCache, - config, - ); + void scheduleHydration(); } }); } return; } default: { - return doHydration(name, props, element, emotionCache, config); + return scheduleHydration(); } } }; diff --git a/dotcom-rendering/src/client/islands/onInteraction.ts b/dotcom-rendering/src/client/islands/onInteraction.ts index d06bc73fd8c..5d6599ca54f 100644 --- a/dotcom-rendering/src/client/islands/onInteraction.ts +++ b/dotcom-rendering/src/client/islands/onInteraction.ts @@ -7,13 +7,13 @@ */ export const onInteraction = ( element: HTMLElement, - callback: (e: HTMLElement) => void, + callback: (e: HTMLElement) => Promise | void, ): void => { element.addEventListener( 'click', (e) => { if (e.target instanceof HTMLElement) { - callback(e.target); + void callback(e.target); } }, { once: true }, diff --git a/dotcom-rendering/src/lib/scheduler.ts b/dotcom-rendering/src/lib/scheduler.ts index fc14138ef09..ee7b243d60d 100644 --- a/dotcom-rendering/src/lib/scheduler.ts +++ b/dotcom-rendering/src/lib/scheduler.ts @@ -1,4 +1,6 @@ import { startPerformanceMeasure } from '@guardian/libs'; +import type { Guard } from './guard'; +import { guard } from './guard'; const START = Date.now(); @@ -22,9 +24,11 @@ let CONCURRENCY_COUNT = Infinity; * priorities, and the scheduler will prefer the priority with the lowest index. **/ const PRIORITIES = ['critical', 'feature', 'enhancement'] as const; -type Priority = (typeof PRIORITIES)[number]; +export type Priority = Guard; export type SchedulePriority = { [K in Priority]: K }; +export const isValidSchedulerPriority = guard(PRIORITIES); + /** * A thing that a consumer want to do. Should be a function that returns a promise. */