streaming.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. // src/jsx/streaming.ts
  2. import { raw } from "../helper/html/index.js";
  3. import { HtmlEscapedCallbackPhase, resolveCallback } from "../utils/html.js";
  4. import { JSXNode } from "./base.js";
  5. import { childrenToString } from "./components.js";
  6. import { DOM_RENDERER, DOM_STASH } from "./constants.js";
  7. import { createContext, useContext } from "./context.js";
  8. import { Suspense as SuspenseDomRenderer } from "./dom/components.js";
  9. import { buildDataStack } from "./dom/render.js";
  10. var StreamingContext = createContext(null);
  11. var suspenseCounter = 0;
  12. var Suspense = async ({
  13. children,
  14. fallback
  15. }) => {
  16. if (!Array.isArray(children)) {
  17. children = [children];
  18. }
  19. const nonce = useContext(StreamingContext)?.scriptNonce;
  20. let resArray = [];
  21. const stackNode = { [DOM_STASH]: [0, []] };
  22. const popNodeStack = (value) => {
  23. buildDataStack.pop();
  24. return value;
  25. };
  26. try {
  27. stackNode[DOM_STASH][0] = 0;
  28. buildDataStack.push([[], stackNode]);
  29. resArray = children.map(
  30. (c) => c == null || typeof c === "boolean" ? "" : c.toString()
  31. );
  32. } catch (e) {
  33. if (e instanceof Promise) {
  34. resArray = [
  35. e.then(() => {
  36. stackNode[DOM_STASH][0] = 0;
  37. buildDataStack.push([[], stackNode]);
  38. return childrenToString(children).then(popNodeStack);
  39. })
  40. ];
  41. } else {
  42. throw e;
  43. }
  44. } finally {
  45. popNodeStack();
  46. }
  47. if (resArray.some((res) => res instanceof Promise)) {
  48. const index = suspenseCounter++;
  49. const fallbackStr = await fallback.toString();
  50. return raw(`<template id="H:${index}"></template>${fallbackStr}<!--/$-->`, [
  51. ...fallbackStr.callbacks || [],
  52. ({ phase, buffer, context }) => {
  53. if (phase === HtmlEscapedCallbackPhase.BeforeStream) {
  54. return;
  55. }
  56. return Promise.all(resArray).then(async (htmlArray) => {
  57. htmlArray = htmlArray.flat();
  58. const content = htmlArray.join("");
  59. if (buffer) {
  60. buffer[0] = buffer[0].replace(
  61. new RegExp(`<template id="H:${index}"></template>.*?<!--/\\$-->`),
  62. content
  63. );
  64. }
  65. let html = buffer ? "" : `<template data-hono-target="H:${index}">${content}</template><script${nonce ? ` nonce="${nonce}"` : ""}>
  66. ((d,c,n) => {
  67. c=d.currentScript.previousSibling
  68. d=d.getElementById('H:${index}')
  69. if(!d)return
  70. do{n=d.nextSibling;n.remove()}while(n.nodeType!=8||n.nodeValue!='/$')
  71. d.replaceWith(c.content)
  72. })(document)
  73. </script>`;
  74. const callbacks = htmlArray.map((html2) => html2.callbacks || []).flat();
  75. if (!callbacks.length) {
  76. return html;
  77. }
  78. if (phase === HtmlEscapedCallbackPhase.Stream) {
  79. html = await resolveCallback(html, HtmlEscapedCallbackPhase.BeforeStream, true, context);
  80. }
  81. return raw(html, callbacks);
  82. });
  83. }
  84. ]);
  85. } else {
  86. return raw(resArray.join(""));
  87. }
  88. };
  89. Suspense[DOM_RENDERER] = SuspenseDomRenderer;
  90. var textEncoder = new TextEncoder();
  91. var renderToReadableStream = (content, onError = console.trace) => {
  92. let cancelled = false;
  93. const reader = new ReadableStream({
  94. async start(controller) {
  95. try {
  96. if (content instanceof JSXNode) {
  97. content = content.toString();
  98. }
  99. const context = typeof content === "object" ? content : {};
  100. const resolved = await resolveCallback(
  101. content,
  102. HtmlEscapedCallbackPhase.BeforeStream,
  103. true,
  104. context
  105. );
  106. if (!cancelled) {
  107. controller.enqueue(textEncoder.encode(resolved));
  108. }
  109. let resolvedCount = 0;
  110. const callbacks = [];
  111. const then = (promise) => {
  112. callbacks.push(
  113. promise.catch((err) => {
  114. console.log(err);
  115. onError(err);
  116. return "";
  117. }).then(async (res) => {
  118. res = await resolveCallback(
  119. res,
  120. HtmlEscapedCallbackPhase.BeforeStream,
  121. true,
  122. context
  123. );
  124. res.callbacks?.map((c) => c({ phase: HtmlEscapedCallbackPhase.Stream, context })).filter(Boolean).forEach(then);
  125. resolvedCount++;
  126. if (!cancelled) {
  127. controller.enqueue(textEncoder.encode(res));
  128. }
  129. })
  130. );
  131. };
  132. resolved.callbacks?.map((c) => c({ phase: HtmlEscapedCallbackPhase.Stream, context })).filter(Boolean).forEach(then);
  133. while (resolvedCount !== callbacks.length) {
  134. await Promise.all(callbacks);
  135. }
  136. } catch (e) {
  137. onError(e);
  138. }
  139. if (!cancelled) {
  140. controller.close();
  141. }
  142. },
  143. cancel() {
  144. cancelled = true;
  145. }
  146. });
  147. return reader;
  148. };
  149. export {
  150. StreamingContext,
  151. Suspense,
  152. renderToReadableStream
  153. };