{"version":3,"file":"transformStreamWithRouter.cjs","sources":["../../src/transformStreamWithRouter.ts"],"sourcesContent":["import { ReadableStream } from 'node:stream/web'\nimport { Readable } from 'node:stream'\nimport { createControlledPromise } from '@tanstack/react-router'\nimport type { AnyRouter } from '@tanstack/react-router'\n\nexport function transformReadableStreamWithRouter(\n  router: AnyRouter,\n  routerStream: ReadableStream,\n) {\n  return transformStreamWithRouter(router, routerStream)\n}\n\nexport function transformPipeableStreamWithRouter(\n  router: AnyRouter,\n  routerStream: Readable,\n) {\n  return Readable.fromWeb(\n    transformStreamWithRouter(router, Readable.toWeb(routerStream)),\n  )\n}\n\n// regex pattern for matching closing body and html tags\nconst patternBodyStart = /(<body)/\nconst patternBodyEnd = /(<\\/body>)/\nconst patternHtmlEnd = /(<\\/html>)/\nconst patternHeadStart = /(<head.*?>)/\n// regex pattern for matching closing tags\nconst patternClosingTag = /(<\\/[a-zA-Z][\\w:.-]*?>)/g\n\nconst textDecoder = new TextDecoder()\n\ntype ReadablePassthrough = {\n  stream: ReadableStream\n  write: (chunk: string) => void\n  end: (chunk?: string) => void\n  destroy: (error: unknown) => void\n  destroyed: boolean\n}\n\nfunction createPassthrough() {\n  let controller: ReadableStreamDefaultController<any>\n  const encoder = new TextEncoder()\n  const stream = new ReadableStream({\n    start(c) {\n      controller = c\n    },\n  })\n\n  const res: ReadablePassthrough = {\n    stream,\n    write: (chunk) => {\n      controller.enqueue(encoder.encode(chunk))\n    },\n    end: (chunk) => {\n      if (chunk) {\n        controller.enqueue(encoder.encode(chunk))\n      }\n      controller.close()\n      res.destroyed = true\n    },\n    destroy: (error) => {\n      controller.error(error)\n    },\n    destroyed: false,\n  }\n\n  return res\n}\n\nasync function readStream(\n  stream: ReadableStream,\n  opts: {\n    onData?: (chunk: ReadableStreamReadValueResult<any>) => void\n    onEnd?: () => void\n    onError?: (error: unknown) => void\n  },\n) {\n  try {\n    const reader = stream.getReader()\n    let chunk\n    while (!(chunk = await reader.read()).done) {\n      opts.onData?.(chunk)\n    }\n    opts.onEnd?.()\n  } catch (error) {\n    opts.onError?.(error)\n  }\n}\n\nexport function transformStreamWithRouter(\n  router: AnyRouter,\n  appStream: ReadableStream,\n) {\n  const finalPassThrough = createPassthrough()\n\n  let isAppRendering = true as boolean\n  let routerStreamBuffer = ''\n  let pendingClosingTags = ''\n  let bodyStarted = false as boolean\n  let headStarted = false as boolean\n  let leftover = ''\n  let leftoverHtml = ''\n\n  function getBufferedRouterStream() {\n    const html = routerStreamBuffer\n    routerStreamBuffer = ''\n    return html\n  }\n\n  function decodeChunk(chunk: unknown): string {\n    if (chunk instanceof Uint8Array) {\n      return textDecoder.decode(chunk)\n    }\n    return String(chunk)\n  }\n\n  const injectedHtmlDonePromise = createControlledPromise<void>()\n\n  let processingCount = 0\n\n  // Process any already-injected HTML\n  router.serverSsr!.injectedHtml.forEach((promise) => {\n    handleInjectedHtml(promise)\n  })\n\n  // Listen for any new injected HTML\n  const stopListeningToInjectedHtml = router.subscribe(\n    'onInjectedHtml',\n    (e) => {\n      handleInjectedHtml(e.promise)\n    },\n  )\n\n  function handleInjectedHtml(promise: Promise<string>) {\n    processingCount++\n\n    promise\n      .then((html) => {\n        if (!bodyStarted) {\n          routerStreamBuffer += html\n        } else {\n          finalPassThrough.write(html)\n        }\n      })\n      .catch(injectedHtmlDonePromise.reject)\n      .finally(() => {\n        processingCount--\n\n        if (!isAppRendering && processingCount === 0) {\n          stopListeningToInjectedHtml()\n          injectedHtmlDonePromise.resolve()\n        }\n      })\n  }\n\n  injectedHtmlDonePromise\n    .then(() => {\n      const finalHtml =\n        leftoverHtml + getBufferedRouterStream() + pendingClosingTags\n\n      finalPassThrough.end(finalHtml)\n    })\n    .catch((err) => {\n      console.error('Error reading routerStream:', err)\n      finalPassThrough.destroy(err)\n    })\n\n  // Transform the appStream\n  readStream(appStream, {\n    onData: (chunk) => {\n      const text = decodeChunk(chunk.value)\n\n      let chunkString = leftover + text\n      const bodyEndMatch = chunkString.match(patternBodyEnd)\n      const htmlEndMatch = chunkString.match(patternHtmlEnd)\n\n      if (!bodyStarted) {\n        const bodyStartMatch = chunkString.match(patternBodyStart)\n        if (bodyStartMatch) {\n          bodyStarted = true\n        }\n      }\n\n      if (!headStarted) {\n        const headStartMatch = chunkString.match(patternHeadStart)\n        if (headStartMatch) {\n          headStarted = true\n          const index = headStartMatch.index!\n          const headTag = headStartMatch[0]\n          const remaining = chunkString.slice(index + headTag.length)\n          finalPassThrough.write(\n            chunkString.slice(0, index) + headTag + getBufferedRouterStream(),\n          )\n          // make sure to only write `remaining` until the next closing tag\n          chunkString = remaining\n        }\n      }\n\n      if (!bodyStarted) {\n        finalPassThrough.write(chunkString)\n        leftover = ''\n        return\n      }\n\n      // If either the body end or html end is in the chunk,\n      // We need to get all of our data in asap\n      if (\n        bodyEndMatch &&\n        htmlEndMatch &&\n        bodyEndMatch.index! < htmlEndMatch.index!\n      ) {\n        const bodyEndIndex = bodyEndMatch.index!\n        pendingClosingTags = chunkString.slice(bodyEndIndex)\n\n        finalPassThrough.write(\n          chunkString.slice(0, bodyEndIndex) + getBufferedRouterStream(),\n        )\n\n        leftover = ''\n        return\n      }\n\n      let result: RegExpExecArray | null\n      let lastIndex = 0\n      while ((result = patternClosingTag.exec(chunkString)) !== null) {\n        lastIndex = result.index + result[0].length\n      }\n\n      if (lastIndex > 0) {\n        const processed =\n          chunkString.slice(0, lastIndex) +\n          getBufferedRouterStream() +\n          leftoverHtml\n\n        finalPassThrough.write(processed)\n        leftover = chunkString.slice(lastIndex)\n      } else {\n        leftover = chunkString\n        leftoverHtml += getBufferedRouterStream()\n      }\n    },\n    onEnd: () => {\n      // Mark the app as done rendering\n      isAppRendering = false\n\n      // If there are no pending promises, resolve the injectedHtmlDonePromise\n      if (processingCount === 0) {\n        injectedHtmlDonePromise.resolve()\n      }\n    },\n    onError: (error) => {\n      console.error('Error reading appStream:', error)\n      finalPassThrough.destroy(error)\n    },\n  })\n\n  return finalPassThrough.stream\n}\n"],"names":["Readable","ReadableStream","createControlledPromise"],"mappings":"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"}