@@ -0,0 +1,127 @@
addEventListener ( "fetch" , event => {
event . respondWith ( handle ( event . request ) )
} )
async function handle ( request ) {
// Fetch from origin server.
let response = await fetch ( request )
// Make sure we only modify text, not images.
let type = response . headers . get ( "Content-Type" ) || ""
if ( ! type . startsWith ( "text/" ) ) {
// Not text. Don't modify.
return response
}
// Create a pipe. The readable side will become our
// new response body.
let { readable, writable } = new TransformStream ( )
// Start processing the body. NOTE: No await!
streamTransformBody ( response . body , writable )
// ... and create our Response while that's running.
return new Response ( readable , response )
}
// A map of template keys to URLs.
const templateMap = {
"BREADCRUMBS" : "https://example.com/bread-crumbs"
}
async function translate ( chunks ) {
const decoder = new TextDecoder ( )
const encoder = new TextEncoder ( )
// Our chunks are in UTF-8, so we need to decode them before
// looking them up in our template map. TextDecoder's streaming
// API makes this easy to perform in a reduction.
let templateKey = chunks . reduce (
( accumulator , chunk ) =>
accumulator + decoder . decode ( chunk , { stream : true } ) ,
"" )
// We need one last call to decoder.decode() to flush
// decoder's buffer. If there's anything left in there, it'll
// come out as Unicode replacement characters.
templateKey += decoder . decode ( )
if ( ! templateMap . hasOwnProperty ( templateKey ) ) {
// We encountered a template key we weren't expecting.
// Just leave its place in the document blank.
return new Uint8Array ( 0 )
}
// We're expecting this template key and know where to find
// its resource.
let response = await fetch ( templateMap [ templateKey ] )
return response . arrayBuffer ( )
}
async function streamTransformBody ( readable , writable ) {
const leftBrace = '{' . charCodeAt ( 0 )
const rightBrace = '}' . charCodeAt ( 0 )
let reader = readable . getReader ( )
let writer = writable . getWriter ( )
// We need to track our state outside the loop in case we
// encounter a template that crosses a chunk boundary.
// Instead of tracking a separate inTemplate boolean, we can
// use the nullity of templateChunks to signal whether we're
// currently in a template.
let templateChunks = null
while ( true ) {
let { done, value } = await reader . read ( )
if ( done ) break
// Each chunk may have zero or more templates, so we'll
// need to loop until we're done processing this chunk.
while ( value . byteLength > 0 ) {
if ( templateChunks ) {
// We're in the middle of a template. Search for the
// terminal brace.
let end = value . indexOf ( rightBrace )
if ( end === - 1 ) {
// This entire chunk is part of a template. No further
// processing of this chunk is necessary.
templateChunks . push ( value )
break
} else {
// We found the termination of a template.
templateChunks . push ( value . subarray ( 0 , end ) )
// Now that we have one complete template, translate it.
await writer . write ( await translate ( templateChunks ) )
templateChunks = null
value = value . subarray ( end + 1 )
}
}
// We're not currently in a template. Search for the
// initial brace.
let start = value . indexOf ( leftBrace )
if ( start === - 1 ) {
// This entire chunk is template-free. We can write
// it and go straight to reading the next one.
await writer . write ( value )
break
} else {
// We found the start of a template -- write the
// chunk up to that point, then continue processing
// the rest of the chunk.
await writer . write ( value . subarray ( 0 , start ) )
value = value . subarray ( start + 1 )
templateChunks = [ ]
}
}
}
// NOTE: If templateChunks is non-null at this point, we
// encountered an unterminated template. This may or may
// not be a problem, depending on your use case.
await writer . close ( )
}