Last active
September 11, 2023 19:14
-
-
Save Overbryd/c070bb1fa769609d404f648cd506340f to your computer and use it in GitHub Desktop.
Revisions
-
Overbryd revised this gist
Jun 26, 2018 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -189,7 +189,7 @@ async function parseFragmentFallback(state, line) { */ async function endParse(state) { if (state.fallbackBuffer !== null) { write(state.writer, state.fallbackBuffer) } } -
Overbryd revised this gist
Jun 26, 2018 . 1 changed file with 1 addition and 2 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -98,8 +98,7 @@ async function transformBody(body, writable, fragments) { const buffer = encoding.decode(value, {stream: !done}) const lines = (lastLine + buffer).split("\n") /* This loop is basically a parse-tree keeping state between each line. * * But most important, is to not include the last line. * The response chunks, might be cut-off just in the middle of a line, and thus not representing -
Overbryd revised this gist
Jun 26, 2018 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -23,8 +23,8 @@ Or multiple `X-Fragments` header work too: < X-Fragments: footer https://yoursite.com/footer.html ``` All prefetches happen asynchronously in parallel. Then in your response body, you can have specific html comments that will be replaced by the prefetched responses. ```html <!DOCTYPE HTML> -
Overbryd revised this gist
Jun 26, 2018 . 1 changed file with 5 additions and 2 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -98,7 +98,8 @@ async function transformBody(body, writable, fragments) { const buffer = encoding.decode(value, {stream: !done}) const lines = (lastLine + buffer).split("\n") /* This loop is highly optimized * Basically it is a parse-tree keeping state between each line. * * But most important, is to not include the last line. * The response chunks, might be cut-off just in the middle of a line, and thus not representing @@ -147,11 +148,13 @@ async function parse(state, line) { const fragmentPromise = state.fragments[key] if (fragmentEnd && fragmentPromise) { await writeFragment(fragmentPromise, state.writer, line + "\n") return [parse, state] } else if (fragmentPromise) { state.fragmentPromise = state.fragments[key] state.fallbackBuffer = "" write(state.writer, line.replace(fragmentStart, "")) return [parseFragmentFallback, state] } } -
Overbryd revised this gist
Jun 26, 2018 . 1 changed file with 1 addition and 2 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -98,8 +98,7 @@ async function transformBody(body, writable, fragments) { const buffer = encoding.decode(value, {stream: !done}) const lines = (lastLine + buffer).split("\n") /* This loop is basically a parse-tree keeping state between each line. * * But most important, is to not include the last line. * The response chunks, might be cut-off just in the middle of a line, and thus not representing -
Overbryd revised this gist
Jun 26, 2018 . 1 changed file with 46 additions and 0 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,46 @@ # Cloudflare fragment rendering/caching This worker script will evaluate your origin response, and replace html comments marked as `fragment:key` with a respective prefetch defined in a `X-Fragments` response header. ## Usage Your origin must include the `X-Fragments` header, specifying the a comma separated list of prefetch requests to make for that response. ``` < HTTP/1.1 200 OK < Content-Type: text/html < Content-Length: 3825 < X-Fragments: header http://localhost:8080/header.html, footer http://localhost:8080/footer.html ``` Or multiple `X-Fragments` header work too: ``` < HTTP/1.1 200 OK < Content-Type: text/html < Content-Length: 3825 < X-Fragments: header https://yoursite.com/header.html < X-Fragments: footer https://yoursite.com/footer.html ``` Then in your response body, you can have specific html comments that will be replaced by the prefetched responses. All prefetches happen asynchronously in parallel. ```html <!DOCTYPE HTML> <html> <head> <!-- fragment:header --> <title>Cloudflare Fragments</title> </head> <body> Some of your body content. <!-- fragment:footer <p>This would be the fallback content if 'footer' does not fetch in time, is unspecified or does not respond successfully</p> --> </body> </html> ``` -
Overbryd revised this gist
Jun 26, 2018 . 1 changed file with 1 addition and 5 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -46,15 +46,10 @@ function prefetchFragments(headers) { }, safeTimeout) }) fragments[key] = Promise.race([ fetch(request), timeout ]) }) return fragments @@ -241,3 +236,4 @@ async function write(writer, str) { const bytes = new TextEncoder('utf-8').encode(str) await writer.write(bytes) } -
Overbryd created this gist
Jun 26, 2018 .There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,243 @@ /* Define regular expressions at top to have them precompiled. */ const htmlContentType = new RegExp('text\/html', 'i') const fragmentStart = new RegExp('<!-- fragment:(\\w+)( -->)?') const commentEnd = new RegExp('-->') addEventListener('fetch', event => { event.respondWith(main(event.request)) }) /* The main entry function */ async function main(request) { const response = await fetch(request) const fragments = prefetchFragments(response.headers) return transformResponse(response, fragments) } /* Build a dictionary of promises that we can evaluate later. * These fetch or timeout. * * The overall timeout is shared by each promise. The cumulative amount of time, that * all fetch-requests can spend is 10 seconds. * * Each fetch request defined in the headers gets a fair share. * We let the promises race and later fail gracefully when the fetch does not return in time. * * This is an important circuit-breaker mechanism, to not fail the main request. */ function prefetchFragments(headers) { const header = headers.get('X-Fragments') if (header === null) return {} const fragments = {} const values = header.split(',') const safeTimeout = 10000 / values.length values.forEach((entry) => { const [key, url] = entry.trim().split(' ') const request = new Request(url) const timeout = new Promise((resolve, reject) => { const wait = setTimeout(() => { clearTimeout(wait) reject() }, safeTimeout) }) /* fragments[key] = Promise.race([ fetch(request), timeout ]) */ //* fragments[key] = fetch(request) //*/ }) return fragments } /* * Here we decide whether we are going to stream & parse the response body, * or just return the response as is, since the request is not eligble for fragments. * * Only Content-Type: text/html responses with one or more fragments are going to be evaluated. */ function transformResponse(response, fragments) { const contentType = response.headers.get('Content-Type') if ( contentType && htmlContentType.test(contentType) && Object.keys(fragments).length > 0 ) { const { readable, writable } = new TransformStream() transformBody(response.body, writable, fragments) return new Response(readable, response) } else { return response } } /* * This function transforms the origin response body. * * It assumes the response to be utf-8 encoded */ async function transformBody(body, writable, fragments) { const encoding = new TextDecoder('utf-8') const reader = body.getReader() const writer = writable.getWriter() // initialise the parser state let state = {writer: writer, fragments: fragments} let fun = parse let lastLine = "" while (true) { const { done, value } = await reader.read() if (done) break const buffer = encoding.decode(value, {stream: !done}) const lines = (lastLine + buffer).split("\n") /* This loop is highly optimized * Basically it is a parse-tree keeping state between each line. * * But most important, is to not include the last line. * The response chunks, might be cut-off just in the middle of a line, and thus not representing * a full line that can be reasoned about. * * Therefore we keep the last line, and concatenate it with the next lines. */ let i = 0; const length = lines.length - 1; for (; i < length; i++) { const line = lines[i] const resp = await fun(state, line) let [nextFun, newState] = resp fun = nextFun state = newState } lastLine = lines[length] || "" } endParse(state) await writer.close() } /* * This is the main parser function. * The state machine goes like this: * * parse * -> ON fragmentMatch with fallback * > parseFragmentFallback * * -> ON fragmentMatch without fallback * > parse * * parseFragmentFallback * -> ON closing comment * > parse */ async function parse(state, line) { const fragmentMatch = line.match(fragmentStart) if (fragmentMatch) { const [match, key, fragmentEnd] = fragmentMatch const fragmentPromise = state.fragments[key] if (fragmentEnd && fragmentPromise) { await writeFragment(fragmentPromise, state.writer, "") return [parse, state] } else if (fragmentPromise) { state.fragmentPromise = state.fragments[key] state.fallbackBuffer = "" return [parseFragmentFallback, state] } } write(state.writer, line + "\n") return [parse, state] } /* * This is a sub-state, that is looking for a closing comment --> to terminate the fallback. * It will keep buffering the response to build the fallback buffer. * * When it finds a `-->` on a line, it will attempt to write the fragment. */ async function parseFragmentFallback(state, line) { if (commentEnd.test(line)) { await writeFragment(state.fragmentPromise, state.writer, state.fallbackBuffer) state.fragmentPromise = null state.fallbackBuffer = null write(state.writer, line.replace(commentEnd, "\n")) return [parse, state] } else { state.fallbackBuffer = state.fallbackBuffer + line + "\n" return [parseFragmentFallback, state] } } /* * This is called after traversing all lines. * If we have accumulated fallback buffer until here, * we might want to dump the response, because someone forgot to add an closing '-->' comment tag. */ async function endParse(state) { if (state.fallbackBuffer !== null) { await writer.write(state.fallbackBuffer) } } /* * This function handles a fragment. * In order for a fragment to render, it must fetch in time and respond with a success state. * * The function will attempt to resolve the promise and pipe any successful response directly * to the main response. Blocking until the fragment response is consumed. * * If the fragment does not respond in time (a timeout happened), we attempt to render a fallback. * * If the fragment response is not succesful, we attempt to render a fallback. */ async function writeFragment(fragmentPromise, writer, fallbackResponse) { try { const fragmentResponse = await fragmentPromise if (fragmentResponse.ok) { await pipe(fragmentResponse.body.getReader(), writer) } else { write(writer, fallbackResponse) } } catch(e) { write(writer, fallbackResponse) } } /* * Helper function to pipe one stream into the other. */ async function pipe(reader, writer) { while (true) { const { done, value } = await reader.read() if (done) break await writer.write(value) } } /* * Helper function to write an utf-8 string to a stream. */ async function write(writer, str) { const bytes = new TextEncoder('utf-8').encode(str) await writer.write(bytes) }