import { prefetchSection, loadSection } from "./section-rendering";
// hover / mouseover
function variantHover(newVariantId: number) {
prefetchSection("product-form", `/products/current-product`, newVariantId);
}
function variantChange(newVariantId: number) {
document.startViewTransition(() => {
loadSection("product-form", `/products/current-product`, newVariantId).then(
(content) => {
const range = document.createRange();
const documentFragment = range.createContextualFragment(content);
// use the new fragment e.g) myElement.replaceWith
// or you can go further and use something like `aralroca/diff-dom-streaming` to diff and only update whats changed
}
);
});
}
Last active
November 7, 2024 02:46
-
-
Save 0x15f/889d4ede331149491ab9189f18cdc0ab to your computer and use it in GitHub Desktop.
Bundled section rendering
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const isDataSavingMode = navigator?.connection?.saveData | |
type SectionQueryParams = URLSearchParams | Record<string, string | number | boolean | null> | |
function convertSectionQueryParamsToURLSearchParams(queryParams: SectionQueryParams) { | |
if (queryParams instanceof URLSearchParams) return queryParams | |
else { | |
const params = new URLSearchParams() | |
for (const [key, value] of Object.entries(queryParams)) { | |
if (value === null) continue | |
params.set(key, value.toString()) | |
} | |
return params | |
} | |
} | |
interface FetchCallbacks { | |
resolve: (data?: string) => void | |
reject: (error: any) => void | |
} | |
class SectionLoader { | |
private pendingRequests = new Map<string, FetchCallbacks[]>() | |
private prefetchRequests = new Set<string>() | |
private prefetchCache = new Map<string, string>() | |
private ongoingFetches = new Set<string>() | |
private fetchSectionsTimeout: ReturnType<typeof setTimeout> | null = null | |
private prefetchSectionsTimeout: ReturnType<typeof setTimeout> | null = null | |
private cacheMaxSize = 100 | |
private buildCacheKey(sectionId: string, path: string, queryParams?: SectionQueryParams): string { | |
return JSON.stringify({ | |
path: path || window.location.pathname, | |
sectionId, | |
queryParams: queryParams ? convertSectionQueryParamsToURLSearchParams(queryParams).toString() : '', | |
}) | |
} | |
private buildRequestGroups(sectionKeys: string[]) { | |
const groups = sectionKeys.reduce( | |
(acc, key) => { | |
const { path, sectionId, queryParams } = JSON.parse(key) | |
const groupKey = JSON.stringify({ | |
path, | |
queryParams, | |
}) | |
const params = queryParams ? new URLSearchParams(queryParams) : undefined | |
if (!acc[groupKey]) { | |
acc[groupKey] = { | |
path, | |
queryParams: params, | |
keys: [], | |
sectionIds: [], | |
} | |
} | |
acc[groupKey].keys.push(key) | |
acc[groupKey].sectionIds.push(sectionId) | |
return acc | |
}, | |
{} as Record< | |
string, | |
{ | |
path: string | |
queryParams?: URLSearchParams | |
keys: string[] | |
sectionIds: string[] | |
} | |
>, | |
) | |
return Object.values(groups) | |
} | |
private scheduleFetchSections() { | |
if (this.fetchSectionsTimeout) { | |
clearTimeout(this.fetchSectionsTimeout) | |
} | |
this.fetchSectionsTimeout = setTimeout(() => this.fetchSections(), 50) | |
} | |
private schedulePrefetchSections() { | |
if (this.prefetchSectionsTimeout) { | |
clearTimeout(this.prefetchSectionsTimeout) | |
} | |
this.prefetchSectionsTimeout = setTimeout(() => this.prefetchSections(), 50) | |
} | |
private async fetchSections() { | |
if (this.pendingRequests.size === 0) { | |
return | |
} | |
const sectionKeys = Array.from(this.pendingRequests.keys()) | |
const requestGroups = this.buildRequestGroups(sectionKeys) | |
await Promise.all( | |
requestGroups.map(async ({ path, queryParams, keys, sectionIds }) => { | |
try { | |
const url = new URL(window.location.href) | |
url.pathname = path || window.location.pathname | |
if (queryParams) { | |
url.search = queryParams.toString() | |
} | |
url.searchParams.set('sections', sectionIds.join(',')) | |
const requestUrl = url.toString() | |
const response = await fetch(requestUrl) | |
if (!response.ok) { | |
throw new Error(`Failed to fetch: ${requestUrl}`) | |
} | |
const sectionsData = await response.json() | |
keys.forEach((key, index) => { | |
const callbacks = this.pendingRequests.get(key) || [] | |
this.pendingRequests.delete(key) | |
const sectionId = sectionIds[index] | |
const data = sectionsData[sectionId] | |
if (data === undefined) { | |
callbacks.forEach(({ reject }) => reject(new Error(`No data for section ${sectionId}`))) | |
} else { | |
callbacks.forEach(({ resolve }) => resolve(data)) | |
this.addToCache(key, data) | |
} | |
this.ongoingFetches.delete(key) | |
}) | |
} catch (error) { | |
console.error(error) | |
keys.forEach((key) => { | |
const callbacks = this.pendingRequests.get(key) || [] | |
this.pendingRequests.delete(key) | |
callbacks.forEach(({ reject }) => reject(error)) | |
this.ongoingFetches.delete(key) | |
}) | |
} | |
}), | |
) | |
} | |
private async prefetchSections() { | |
if (this.prefetchRequests.size === 0) { | |
return | |
} | |
const sectionKeys = Array.from(this.prefetchRequests) | |
const requestGroups = this.buildRequestGroups(sectionKeys) | |
await Promise.all( | |
requestGroups.map(async ({ path, queryParams, keys, sectionIds }) => { | |
try { | |
const url = new URL(window.location.href) | |
url.pathname = path || window.location.pathname | |
if (queryParams) { | |
url.search = queryParams.toString() | |
} | |
url.searchParams.set('sections', sectionIds.join(',')) | |
const requestUrl = url.toString() | |
const response = await fetch(requestUrl, { | |
priority: 'low', | |
}) | |
if (!response.ok) { | |
throw new Error(`Failed to prefetch: ${requestUrl}`) | |
} | |
const sectionsData = await response.json() | |
keys.forEach((key, index) => { | |
const sectionId = sectionIds[index] | |
const data = sectionsData[sectionId] | |
if (data !== undefined) { | |
this.addToCache(key, data) | |
} | |
this.prefetchRequests.delete(key) | |
this.ongoingFetches.delete(key) | |
const callbacks = this.pendingRequests.get(key) || [] | |
this.pendingRequests.delete(key) | |
callbacks.forEach(({ resolve, reject }) => { | |
if (data !== undefined) { | |
resolve(data) | |
} else { | |
reject(new Error(`No data for section ${sectionId}`)) | |
} | |
}) | |
}) | |
} catch (error) { | |
console.error(error) | |
keys.forEach((key) => { | |
this.prefetchRequests.delete(key) | |
this.ongoingFetches.delete(key) | |
const callbacks = this.pendingRequests.get(key) || [] | |
this.pendingRequests.delete(key) | |
callbacks.forEach(({ reject }) => reject(error)) | |
}) | |
} | |
}), | |
) | |
} | |
private addToCache(key: string, data: string) { | |
if (this.prefetchCache.size > this.cacheMaxSize) { | |
this.prefetchCache.delete(this.prefetchCache.keys().next().value) | |
} | |
this.prefetchCache.set(key, data) | |
} | |
public loadSection( | |
sectionId: string, | |
path: string = '', | |
queryParams?: SectionQueryParams, | |
): Promise<string | undefined> { | |
const key = this.buildCacheKey(sectionId, path, queryParams) | |
if (this.prefetchCache.has(key)) { | |
return Promise.resolve(this.prefetchCache.get(key)) | |
} | |
return new Promise((resolve, reject) => { | |
if (!this.pendingRequests.has(key)) { | |
this.pendingRequests.set(key, [{ resolve, reject }]) | |
} | |
if (!this.ongoingFetches.has(key) && !this.prefetchRequests.has(key)) { | |
this.ongoingFetches.add(key) | |
this.scheduleFetchSections() | |
} | |
}) | |
} | |
public prefetchSection(sectionId: string, path: string = '', queryParams?: SectionQueryParams) { | |
if (isDataSavingMode) { | |
return | |
} | |
const key = this.buildCacheKey(sectionId, path, queryParams) | |
if ( | |
this.pendingRequests.has(key) || | |
this.ongoingFetches.has(key) || | |
this.prefetchCache.has(key) || | |
this.prefetchRequests.has(key) | |
) { | |
return | |
} | |
this.prefetchRequests.add(key) | |
this.ongoingFetches.add(key) | |
this.schedulePrefetchSections() | |
} | |
public cancelPrefetchSection(sectionId: string, path: string = '', queryParams?: SectionQueryParams) { | |
this.prefetchRequests.delete(this.buildCacheKey(sectionId, path, queryParams)) | |
if (this.prefetchRequests.size) { | |
this.schedulePrefetchSections() | |
} | |
} | |
} | |
const sectionLoader = new SectionLoader() | |
export const loadSection = sectionLoader.loadSection.bind(sectionLoader) | |
export const prefetchSection = sectionLoader.prefetchSection.bind(sectionLoader) | |
export const cancelPrefetchSection = sectionLoader.cancelPrefetchSection.bind(sectionLoader) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment