From 70320cbdeb1b3a1de84c6c3417b64141920c5e50 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Fri, 17 Jan 2025 16:30:38 +0100 Subject: [PATCH] add fragment cache (to match turbo cache) --- packages/react/src/root.test.tsx | 28 +++++++++++++++++ packages/react/src/root.ts | 54 ++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/packages/react/src/root.test.tsx b/packages/react/src/root.test.tsx index b8a23db..39e2976 100644 --- a/packages/react/src/root.test.tsx +++ b/packages/react/src/root.test.tsx @@ -97,6 +97,34 @@ describe('@coldwired/react', () => { root.destroy(); }); + it('render fragment with component and cache', async () => { + document.body.innerHTML = `<${DEFAULT_TAG_NAME}><${REACT_COMPONENT_TAG} ${NAME_ATTRIBUTE}="Counter">`; + const root = createRoot({ + loader: (name) => Promise.resolve(manifest[name]), + cache: true, + }); + await root.render(document.body).done; + + expect(document.body.innerHTML).toEqual( + `<${DEFAULT_TAG_NAME} data-fragment-id="fragment-0">

Count: 0

`, + ); + + root.destroy(); + + { + const root = createRoot({ + loader: (name) => Promise.resolve(manifest[name]), + cache: true, + }); + await root.render(document.body).done; + + expect(document.body.innerHTML).toEqual( + `<${DEFAULT_TAG_NAME} data-fragment-id="fragment-0">

Count: 0

`, + ); + root.destroy(); + } + }); + it('render with error boundary', async () => { document.body.innerHTML = `<${DEFAULT_TAG_NAME}> <${REACT_COMPONENT_TAG} ${NAME_ATTRIBUTE}="Counter"> diff --git a/packages/react/src/root.ts b/packages/react/src/root.ts index 520b1e9..5a85629 100644 --- a/packages/react/src/root.ts +++ b/packages/react/src/root.ts @@ -32,6 +32,7 @@ export interface RootOptions { schema?: Partial; layoutComponentName?: string; errorBoundaryFallbackComponentName?: string; + cache?: boolean; } export interface Schema extends TreeBuilderSchema { @@ -115,6 +116,11 @@ export function createRoot( } await preload(fragment, (names) => manifestLoader(names, loader, manifest), schema); const tree = hydrate(fragment, manifest, schema); + + if (options.cache) { + saveFragmentCache(element, fragmentOrHTML); + } + if (reset) { element.innerHTML = ''; } @@ -145,6 +151,9 @@ export function createRoot( await mounting; mounted.set(element, (fragmentOrHTML) => render(element, fragmentOrHTML, false)); registerDestroyer(element); + if (options.cache) { + restoreFragmentCache(element); + } await render(element, element, true); } }; @@ -278,3 +287,48 @@ async function getErrorBoundaryFallbackComponent( } return; } + +let fragmentCacheIdSequence = 0; +const fragmentCacheIdAttributeName = 'data-fragment-id'; +const fragmentCache = new Map(); + +export function resetFragmentCache() { + fragmentCache.clear(); +} + +function saveFragmentCache(element: Element, fragment: DocumentFragmentLike | string) { + let fragmentCacheId = element.getAttribute(fragmentCacheIdAttributeName); + if (!fragmentCacheId) { + fragmentCacheId = `fragment-${fragmentCacheIdSequence++}`; + element.setAttribute(fragmentCacheIdAttributeName, fragmentCacheId); + } + fragmentCache.set(fragmentCacheId, stringifyFragment(fragment)); +} + +function restoreFragmentCache(element: Element) { + const fragmentCacheId = element.getAttribute(fragmentCacheIdAttributeName); + if (fragmentCacheId) { + const html = fragmentCache.get(fragmentCacheId); + if (html) { + element.innerHTML = html; + } + } +} + +function stringifyFragment(fragmentOrHTML: DocumentFragmentLike | string): string { + if (typeof fragmentOrHTML == 'string') { + return fragmentOrHTML; + } + if (isElement(fragmentOrHTML)) { + return fragmentOrHTML.innerHTML; + } + const html: string[] = []; + for (const node of fragmentOrHTML.childNodes) { + if (isElement(node)) { + html.push(node.outerHTML); + } else if (node.nodeType == Node.TEXT_NODE && node.textContent) { + html.push(node.textContent); + } + } + return html.join(' '); +}