@tanstack/redact 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/dist/core/index.d.ts +2 -0
  2. package/dist/core/index.js +4 -0
  3. package/dist/core/index.js.map +7 -0
  4. package/dist/core/internal.d.ts +76 -0
  5. package/dist/core/internal.js +59 -0
  6. package/dist/core/internal.js.map +7 -0
  7. package/dist/core/types.d.ts +21 -0
  8. package/dist/core/types.js +10 -0
  9. package/dist/core/types.js.map +7 -0
  10. package/dist/dom/_all.d.ts +17 -0
  11. package/dist/dom/_all.js +51 -0
  12. package/dist/dom/_all.js.map +7 -0
  13. package/dist/dom/client.d.ts +3 -0
  14. package/dist/dom/client.js +8 -0
  15. package/dist/dom/client.js.map +7 -0
  16. package/dist/dom/dispatcher.d.ts +20 -0
  17. package/dist/dom/dispatcher.js +267 -0
  18. package/dist/dom/dispatcher.js.map +7 -0
  19. package/dist/dom/dom.d.ts +10 -0
  20. package/dist/dom/dom.js +262 -0
  21. package/dist/dom/dom.js.map +7 -0
  22. package/dist/dom/event-replay.d.ts +13 -0
  23. package/dist/dom/event-replay.js +46 -0
  24. package/dist/dom/event-replay.js.map +7 -0
  25. package/dist/dom/features/class/full.d.ts +1 -0
  26. package/dist/dom/features/class/full.js +91 -0
  27. package/dist/dom/features/class/full.js.map +7 -0
  28. package/dist/dom/features/class/index.d.ts +1 -0
  29. package/dist/dom/features/class/index.js +3 -0
  30. package/dist/dom/features/class/index.js.map +7 -0
  31. package/dist/dom/features/class/stub.d.ts +1 -0
  32. package/dist/dom/features/class/stub.js +50 -0
  33. package/dist/dom/features/class/stub.js.map +7 -0
  34. package/dist/dom/features/context/full.d.ts +1 -0
  35. package/dist/dom/features/context/full.js +49 -0
  36. package/dist/dom/features/context/full.js.map +7 -0
  37. package/dist/dom/features/context/index.d.ts +1 -0
  38. package/dist/dom/features/context/index.js +3 -0
  39. package/dist/dom/features/context/index.js.map +7 -0
  40. package/dist/dom/features/context/stub.d.ts +1 -0
  41. package/dist/dom/features/context/stub.js +22 -0
  42. package/dist/dom/features/context/stub.js.map +7 -0
  43. package/dist/dom/features/forward-ref/full.d.ts +1 -0
  44. package/dist/dom/features/forward-ref/full.js +51 -0
  45. package/dist/dom/features/forward-ref/full.js.map +7 -0
  46. package/dist/dom/features/forward-ref/index.d.ts +1 -0
  47. package/dist/dom/features/forward-ref/index.js +3 -0
  48. package/dist/dom/features/forward-ref/index.js.map +7 -0
  49. package/dist/dom/features/forward-ref/stub.d.ts +1 -0
  50. package/dist/dom/features/forward-ref/stub.js +22 -0
  51. package/dist/dom/features/forward-ref/stub.js.map +7 -0
  52. package/dist/dom/features/hydration/full.d.ts +61 -0
  53. package/dist/dom/features/hydration/full.js +264 -0
  54. package/dist/dom/features/hydration/full.js.map +7 -0
  55. package/dist/dom/features/hydration/index.d.ts +1 -0
  56. package/dist/dom/features/hydration/index.js +3 -0
  57. package/dist/dom/features/hydration/index.js.map +7 -0
  58. package/dist/dom/features/hydration/stub.d.ts +28 -0
  59. package/dist/dom/features/hydration/stub.js +66 -0
  60. package/dist/dom/features/hydration/stub.js.map +7 -0
  61. package/dist/dom/features/index.d.ts +7 -0
  62. package/dist/dom/features/index.js +9 -0
  63. package/dist/dom/features/index.js.map +7 -0
  64. package/dist/dom/features/lazy/full.d.ts +1 -0
  65. package/dist/dom/features/lazy/full.js +78 -0
  66. package/dist/dom/features/lazy/full.js.map +7 -0
  67. package/dist/dom/features/lazy/index.d.ts +1 -0
  68. package/dist/dom/features/lazy/index.js +3 -0
  69. package/dist/dom/features/lazy/index.js.map +7 -0
  70. package/dist/dom/features/lazy/stub.d.ts +1 -0
  71. package/dist/dom/features/lazy/stub.js +36 -0
  72. package/dist/dom/features/lazy/stub.js.map +7 -0
  73. package/dist/dom/features/memo/full.d.ts +1 -0
  74. package/dist/dom/features/memo/full.js +59 -0
  75. package/dist/dom/features/memo/full.js.map +7 -0
  76. package/dist/dom/features/memo/index.d.ts +1 -0
  77. package/dist/dom/features/memo/index.js +3 -0
  78. package/dist/dom/features/memo/index.js.map +7 -0
  79. package/dist/dom/features/memo/stub.d.ts +1 -0
  80. package/dist/dom/features/memo/stub.js +36 -0
  81. package/dist/dom/features/memo/stub.js.map +7 -0
  82. package/dist/dom/features/portal/full.d.ts +1 -0
  83. package/dist/dom/features/portal/full.js +19 -0
  84. package/dist/dom/features/portal/full.js.map +7 -0
  85. package/dist/dom/features/portal/index.d.ts +1 -0
  86. package/dist/dom/features/portal/index.js +3 -0
  87. package/dist/dom/features/portal/index.js.map +7 -0
  88. package/dist/dom/features/portal/stub.d.ts +1 -0
  89. package/dist/dom/features/portal/stub.js +7 -0
  90. package/dist/dom/features/portal/stub.js.map +7 -0
  91. package/dist/dom/features/suspense/full.d.ts +1 -0
  92. package/dist/dom/features/suspense/full.js +146 -0
  93. package/dist/dom/features/suspense/full.js.map +7 -0
  94. package/dist/dom/features/suspense/index.d.ts +1 -0
  95. package/dist/dom/features/suspense/index.js +3 -0
  96. package/dist/dom/features/suspense/index.js.map +7 -0
  97. package/dist/dom/features/suspense/stub.d.ts +1 -0
  98. package/dist/dom/features/suspense/stub.js +6 -0
  99. package/dist/dom/features/suspense/stub.js.map +7 -0
  100. package/dist/dom/index.d.ts +25 -0
  101. package/dist/dom/index.js +45 -0
  102. package/dist/dom/index.js.map +7 -0
  103. package/dist/dom/portal.d.ts +2 -0
  104. package/dist/dom/portal.js +15 -0
  105. package/dist/dom/portal.js.map +7 -0
  106. package/dist/dom/reconcile.d.ts +40 -0
  107. package/dist/dom/reconcile.js +795 -0
  108. package/dist/dom/reconcile.js.map +7 -0
  109. package/dist/dom/root.d.ts +15 -0
  110. package/dist/dom/root.js +85 -0
  111. package/dist/dom/root.js.map +7 -0
  112. package/dist/dom/test-utils.d.ts +1 -0
  113. package/dist/dom/test-utils.js +10 -0
  114. package/dist/dom/test-utils.js.map +7 -0
  115. package/dist/react/children.d.ts +8 -0
  116. package/dist/react/children.js +75 -0
  117. package/dist/react/children.js.map +7 -0
  118. package/dist/react/class.d.ts +31 -0
  119. package/dist/react/class.js +40 -0
  120. package/dist/react/class.js.map +7 -0
  121. package/dist/react/context.d.ts +27 -0
  122. package/dist/react/context.js +31 -0
  123. package/dist/react/context.js.map +7 -0
  124. package/dist/react/element.d.ts +10 -0
  125. package/dist/react/element.js +64 -0
  126. package/dist/react/element.js.map +7 -0
  127. package/dist/react/hooks.d.ts +37 -0
  128. package/dist/react/hooks.js +146 -0
  129. package/dist/react/hooks.js.map +7 -0
  130. package/dist/react/index.d.ts +76 -0
  131. package/dist/react/index.js +187 -0
  132. package/dist/react/index.js.map +7 -0
  133. package/dist/react/jsx-runtime.d.ts +5 -0
  134. package/dist/react/jsx-runtime.js +21 -0
  135. package/dist/react/jsx-runtime.js.map +7 -0
  136. package/dist/react/memo.d.ts +29 -0
  137. package/dist/react/memo.js +47 -0
  138. package/dist/react/memo.js.map +7 -0
  139. package/dist/react/portal.d.ts +1 -0
  140. package/dist/react/portal.js +6 -0
  141. package/dist/react/portal.js.map +7 -0
  142. package/dist/react/shared-internals.d.ts +33 -0
  143. package/dist/react/shared-internals.js +29 -0
  144. package/dist/react/shared-internals.js.map +7 -0
  145. package/dist/react/suspense.d.ts +15 -0
  146. package/dist/react/suspense.js +16 -0
  147. package/dist/react/suspense.js.map +7 -0
  148. package/dist/scheduler/index.d.ts +44 -0
  149. package/dist/scheduler/index.js +109 -0
  150. package/dist/scheduler/index.js.map +7 -0
  151. package/dist/server/bootstrap-script.d.ts +22 -0
  152. package/dist/server/bootstrap-script.js +16 -0
  153. package/dist/server/bootstrap-script.js.map +7 -0
  154. package/dist/server/dispatcher.d.ts +37 -0
  155. package/dist/server/dispatcher.js +140 -0
  156. package/dist/server/dispatcher.js.map +7 -0
  157. package/dist/server/escape.d.ts +7 -0
  158. package/dist/server/escape.js +159 -0
  159. package/dist/server/escape.js.map +7 -0
  160. package/dist/server/index.d.ts +16 -0
  161. package/dist/server/index.js +26 -0
  162. package/dist/server/index.js.map +7 -0
  163. package/dist/server/renderToString.d.ts +7 -0
  164. package/dist/server/renderToString.js +29 -0
  165. package/dist/server/renderToString.js.map +7 -0
  166. package/dist/server/stream.d.ts +40 -0
  167. package/dist/server/stream.js +200 -0
  168. package/dist/server/stream.js.map +7 -0
  169. package/dist/server/walk.d.ts +33 -0
  170. package/dist/server/walk.js +298 -0
  171. package/dist/server/walk.js.map +7 -0
  172. package/dist/vite/index.d.ts +96 -0
  173. package/dist/vite/index.js +217 -0
  174. package/dist/vite/index.js.map +7 -0
  175. package/package.json +83 -0
  176. package/src/core/index.ts +2 -0
  177. package/src/core/internal.ts +107 -0
  178. package/src/core/types.ts +36 -0
  179. package/src/dom/_all.ts +54 -0
  180. package/src/dom/client.ts +4 -0
  181. package/src/dom/dispatcher.ts +318 -0
  182. package/src/dom/dom.ts +316 -0
  183. package/src/dom/event-replay.ts +55 -0
  184. package/src/dom/features/class/full.ts +104 -0
  185. package/src/dom/features/class/index.ts +1 -0
  186. package/src/dom/features/class/stub.ts +65 -0
  187. package/src/dom/features/context/full.ts +57 -0
  188. package/src/dom/features/context/index.ts +1 -0
  189. package/src/dom/features/context/stub.ts +32 -0
  190. package/src/dom/features/forward-ref/full.ts +54 -0
  191. package/src/dom/features/forward-ref/index.ts +1 -0
  192. package/src/dom/features/forward-ref/stub.ts +28 -0
  193. package/src/dom/features/hydration/full.ts +345 -0
  194. package/src/dom/features/hydration/index.ts +1 -0
  195. package/src/dom/features/hydration/stub.ts +72 -0
  196. package/src/dom/features/index.ts +11 -0
  197. package/src/dom/features/lazy/full.ts +98 -0
  198. package/src/dom/features/lazy/index.ts +1 -0
  199. package/src/dom/features/lazy/stub.ts +45 -0
  200. package/src/dom/features/memo/full.ts +74 -0
  201. package/src/dom/features/memo/index.ts +1 -0
  202. package/src/dom/features/memo/stub.ts +42 -0
  203. package/src/dom/features/portal/full.ts +22 -0
  204. package/src/dom/features/portal/index.ts +1 -0
  205. package/src/dom/features/portal/stub.ts +11 -0
  206. package/src/dom/features/suspense/full.ts +191 -0
  207. package/src/dom/features/suspense/index.ts +1 -0
  208. package/src/dom/features/suspense/stub.ts +12 -0
  209. package/src/dom/index.ts +33 -0
  210. package/src/dom/portal.ts +16 -0
  211. package/src/dom/reconcile.ts +1169 -0
  212. package/src/dom/root.ts +107 -0
  213. package/src/dom/test-utils.ts +6 -0
  214. package/src/react/children.ts +85 -0
  215. package/src/react/class.ts +56 -0
  216. package/src/react/context.ts +50 -0
  217. package/src/react/element.ts +80 -0
  218. package/src/react/hooks.ts +190 -0
  219. package/src/react/index.ts +132 -0
  220. package/src/react/jsx-runtime.ts +16 -0
  221. package/src/react/memo.ts +45 -0
  222. package/src/react/portal.ts +1 -0
  223. package/src/react/shared-internals.ts +79 -0
  224. package/src/react/suspense.ts +18 -0
  225. package/src/scheduler/index.ts +124 -0
  226. package/src/server/bootstrap-script.ts +52 -0
  227. package/src/server/dispatcher.ts +158 -0
  228. package/src/server/escape.ts +187 -0
  229. package/src/server/index.ts +26 -0
  230. package/src/server/renderToString.ts +33 -0
  231. package/src/server/stream.ts +302 -0
  232. package/src/server/walk.ts +448 -0
  233. package/src/vite/index.ts +401 -0
@@ -0,0 +1,26 @@
1
+ export { renderToString, renderToStaticMarkup } from './renderToString'
2
+ export { renderToReadableStream, renderToPipeableStream } from './stream'
3
+ export type {
4
+ StreamOptions,
5
+ PipeableOptions,
6
+ PipeableHandle,
7
+ ReadableStreamResult,
8
+ } from './stream'
9
+ export { BOUNDARY_REVEAL_RUNTIME } from './bootstrap-script'
10
+
11
+ export const version = '19.2.3'
12
+
13
+ // Default export — React's real `react-dom/server` ships one, and TanStack
14
+ // Router's SSR code does `import ReactDOMServer from 'react-dom/server'` and
15
+ // reads `.renderToReadableStream` off the default.
16
+ import { renderToString, renderToStaticMarkup } from './renderToString'
17
+ import { renderToReadableStream, renderToPipeableStream } from './stream'
18
+ import { BOUNDARY_REVEAL_RUNTIME } from './bootstrap-script'
19
+ export default {
20
+ renderToString,
21
+ renderToStaticMarkup,
22
+ renderToReadableStream,
23
+ renderToPipeableStream,
24
+ BOUNDARY_REVEAL_RUNTIME,
25
+ version: '19.2.3',
26
+ }
@@ -0,0 +1,33 @@
1
+ import type { ReactNode } from '../core'
2
+ import { beginSSR, endSSR, installSSRDispatcher, uninstallSSRDispatcher } from './dispatcher'
3
+ import { walk } from './walk'
4
+
5
+ export function renderToString(
6
+ children: ReactNode,
7
+ options: { identifierPrefix?: string } = {},
8
+ ): string {
9
+ installSSRDispatcher()
10
+ beginSSR(options.identifierPrefix)
11
+ let output = ''
12
+ let id = 0
13
+ try {
14
+ walk(children, {
15
+ emit: (s) => {
16
+ output += s
17
+ },
18
+ nextBoundaryId: () => id++,
19
+ })
20
+ } finally {
21
+ endSSR()
22
+ uninstallSSRDispatcher()
23
+ }
24
+ return output
25
+ }
26
+
27
+ export function renderToStaticMarkup(
28
+ children: ReactNode,
29
+ options: { identifierPrefix?: string } = {},
30
+ ): string {
31
+ // For our purposes identical to renderToString (no hydration markers in static output)
32
+ return renderToString(children, options).replace(/<!--[^>]*-->/g, '')
33
+ }
@@ -0,0 +1,302 @@
1
+ import type { ReactNode } from '../core'
2
+ import {
3
+ beginSSR,
4
+ endSSR,
5
+ installSSRDispatcher,
6
+ uninstallSSRDispatcher,
7
+ applyContextSnapshot,
8
+ } from './dispatcher'
9
+ import { walk, type SuspendedBoundary } from './walk'
10
+ import { BOUNDARY_REVEAL_RUNTIME, revealScript } from './bootstrap-script'
11
+
12
+ export interface StreamOptions {
13
+ identifierPrefix?: string
14
+ nonce?: string
15
+ bootstrapScripts?: ReadonlyArray<string | { src: string; async?: boolean; nonce?: string }>
16
+ bootstrapModules?: ReadonlyArray<string | { src: string; nonce?: string }>
17
+ onError?: (error: unknown) => string | void
18
+ signal?: AbortSignal
19
+ progressiveChunkSize?: number
20
+ }
21
+
22
+ export interface ReadableStreamResult extends ReadableStream<Uint8Array> {
23
+ allReady: Promise<void>
24
+ }
25
+
26
+ export interface OrchestratorState {
27
+ nextId: number
28
+ pending: Set<Promise<void>>
29
+ closed: boolean
30
+ errored: unknown | null
31
+ }
32
+
33
+ type Emit = (chunk: string) => void
34
+
35
+ export async function streamHtml(
36
+ children: ReactNode,
37
+ emit: Emit,
38
+ options: StreamOptions,
39
+ state: OrchestratorState,
40
+ ): Promise<void> {
41
+ installSSRDispatcher()
42
+ beginSSR(options.identifierPrefix)
43
+ const nonce = options.nonce
44
+
45
+ try {
46
+ const boundaries: SuspendedBoundary[] = []
47
+
48
+ // Buffer shell + bootstrap into a string[] and flush as a single emit.
49
+ // Per-emit overhead in renderToReadableStream is TextEncoder.encode +
50
+ // controller.enqueue — each walkHost normally fires 3+ emits (opening
51
+ // tag, per-attribute, closing bracket), which for a ~30-component tree
52
+ // is ~100 stream-controller round-trips. Batching collapses those into
53
+ // one encode and one enqueue per shell — measured ~2-4% of total SSR
54
+ // time on CPU profiles.
55
+ const shellChunks: string[] = []
56
+ const bufferedEmit: Emit = (chunk) => {
57
+ shellChunks.push(chunk)
58
+ }
59
+
60
+ // 1. Render the shell
61
+ walk(children, {
62
+ emit: bufferedEmit,
63
+ onSuspend: (b) => boundaries.push(b),
64
+ nextBoundaryId: () => state.nextId++,
65
+ })
66
+
67
+ // 2. Inject runtime + bootstrap scripts (once, after shell). Skip the
68
+ // reveal/event-replay runtime when nothing needs it — no suspensions to
69
+ // reveal and no bootstrap scripts to guard against early user input. That
70
+ // keeps fully-static SSR responses byte-equivalent to a plain walk and
71
+ // matches React's behavior where `renderToReadableStream` of a static
72
+ // tree emits only the markup.
73
+ const hasBootstrap =
74
+ (options.bootstrapScripts?.length ?? 0) > 0 ||
75
+ (options.bootstrapModules?.length ?? 0) > 0
76
+ if (boundaries.length > 0 || hasBootstrap) {
77
+ shellChunks.push(`<script${nonce ? ` nonce="${nonce}"` : ''}>${BOUNDARY_REVEAL_RUNTIME}</script>`)
78
+ for (const s of options.bootstrapScripts ?? []) {
79
+ shellChunks.push(bootstrapTag(s, 'script', nonce))
80
+ }
81
+ for (const m of options.bootstrapModules ?? []) {
82
+ shellChunks.push(bootstrapTag(m, 'module', nonce))
83
+ }
84
+ }
85
+
86
+ if (shellChunks.length) emit(shellChunks.join(''))
87
+
88
+ // 3. Stream suspended boundaries as they resolve
89
+ for (const b of boundaries) streamBoundary(b, emit, options, state)
90
+ await drain(state)
91
+ } catch (err) {
92
+ state.errored = err
93
+ if (options.onError) options.onError(err)
94
+ throw err
95
+ } finally {
96
+ endSSR()
97
+ uninstallSSRDispatcher()
98
+ state.closed = true
99
+ }
100
+ }
101
+
102
+ function streamBoundary(
103
+ b: SuspendedBoundary,
104
+ emit: Emit,
105
+ options: StreamOptions,
106
+ state: OrchestratorState,
107
+ ): void {
108
+ const task = (async () => {
109
+ try {
110
+ await b.thenable
111
+ } catch (err) {
112
+ if (options.onError) options.onError(err)
113
+ }
114
+ if (state.closed) return
115
+
116
+ // Re-render the boundary's children into a string, restoring the
117
+ // provider stack from when the boundary first suspended.
118
+ const parts: string[] = []
119
+ const sub: SuspendedBoundary[] = []
120
+ const restore = applyContextSnapshot(b.contextSnapshot)
121
+ try {
122
+ walk(b.children, {
123
+ emit: (s) => parts.push(s),
124
+ onSuspend: (n) => sub.push(n),
125
+ nextBoundaryId: () => state.nextId++,
126
+ })
127
+ } catch (err) {
128
+ if (options.onError) options.onError(err)
129
+ restore()
130
+ return
131
+ }
132
+ restore()
133
+
134
+ emit(`<div hidden id="S:${b.id}">${parts.join('')}</div>${revealScript(b.id, options.nonce)}`)
135
+
136
+ // Recurse: any nested suspensions inside the now-revealed content
137
+ for (const s of sub) streamBoundary(s, emit, options, state)
138
+ })()
139
+ state.pending.add(task)
140
+ task.finally(() => state.pending.delete(task))
141
+ }
142
+
143
+ async function drain(state: OrchestratorState): Promise<void> {
144
+ while (state.pending.size > 0) {
145
+ await Promise.race(state.pending)
146
+ }
147
+ }
148
+
149
+ function bootstrapTag(
150
+ entry: string | { src: string; async?: boolean; nonce?: string },
151
+ kind: 'script' | 'module',
152
+ defaultNonce: string | undefined,
153
+ ): string {
154
+ const src = typeof entry === 'string' ? entry : entry.src
155
+ const nonce = typeof entry === 'string' ? defaultNonce : entry.nonce ?? defaultNonce
156
+ const nAttr = nonce ? ` nonce="${nonce}"` : ''
157
+ if (kind === 'module') return `<script type="module"${nAttr} src="${src}"></script>`
158
+ return `<script async${nAttr} src="${src}"></script>`
159
+ }
160
+
161
+ // ---------------------------------------------------------------------------
162
+ // Web Streams: renderToReadableStream
163
+ // ---------------------------------------------------------------------------
164
+
165
+ export function renderToReadableStream(
166
+ children: ReactNode,
167
+ options: StreamOptions = {},
168
+ ): Promise<ReadableStreamResult> {
169
+ const state: OrchestratorState = {
170
+ nextId: 0,
171
+ pending: new Set(),
172
+ closed: false,
173
+ errored: null,
174
+ }
175
+ const encoder = new TextEncoder()
176
+
177
+ let allReadyResolve!: () => void
178
+ let allReadyReject!: (e: unknown) => void
179
+ const allReady = new Promise<void>((r, rej) => {
180
+ allReadyResolve = r
181
+ allReadyReject = rej
182
+ })
183
+
184
+ const stream = new ReadableStream<Uint8Array>({
185
+ start(controller) {
186
+ const emit = (chunk: string) => {
187
+ try {
188
+ controller.enqueue(encoder.encode(chunk))
189
+ } catch {}
190
+ }
191
+
192
+ streamHtml(children, emit, options, state).then(
193
+ () => {
194
+ try {
195
+ controller.close()
196
+ } catch {}
197
+ allReadyResolve()
198
+ },
199
+ (err) => {
200
+ try {
201
+ controller.error(err)
202
+ } catch {}
203
+ allReadyReject(err)
204
+ },
205
+ )
206
+
207
+ options.signal?.addEventListener('abort', () => {
208
+ state.closed = true
209
+ try {
210
+ controller.close()
211
+ } catch {}
212
+ })
213
+ },
214
+ })
215
+
216
+ return Promise.resolve(Object.assign(stream, { allReady }))
217
+ }
218
+
219
+ // ---------------------------------------------------------------------------
220
+ // Node Streams: renderToPipeableStream
221
+ // ---------------------------------------------------------------------------
222
+
223
+ export interface PipeableHandle {
224
+ pipe<T extends NodeJS.WritableStream>(dest: T): T
225
+ abort(reason?: unknown): void
226
+ }
227
+
228
+ export interface PipeableOptions extends StreamOptions {
229
+ onShellReady?: () => void
230
+ onShellError?: (err: unknown) => void
231
+ onAllReady?: () => void
232
+ }
233
+
234
+ export function renderToPipeableStream(
235
+ children: ReactNode,
236
+ options: PipeableOptions = {},
237
+ ): PipeableHandle {
238
+ const state: OrchestratorState = {
239
+ nextId: 0,
240
+ pending: new Set(),
241
+ closed: false,
242
+ errored: null,
243
+ }
244
+
245
+ const buffers: string[] = []
246
+ let dest: NodeJS.WritableStream | null = null
247
+ let shellReady = false
248
+ let finished = false
249
+ let aborted = false
250
+
251
+ const flushTo = (w: NodeJS.WritableStream) => {
252
+ if (!buffers.length) return
253
+ for (const b of buffers) w.write(b)
254
+ buffers.length = 0
255
+ }
256
+
257
+ const emit: Emit = (chunk) => {
258
+ if (aborted || finished) return
259
+ if (dest) dest.write(chunk)
260
+ else buffers.push(chunk)
261
+ }
262
+
263
+ // Kick off rendering
264
+ streamHtml(children, emit, options, state).then(
265
+ () => {
266
+ finished = true
267
+ if (dest) dest.end()
268
+ options.onAllReady?.()
269
+ },
270
+ (err) => {
271
+ if (!shellReady) {
272
+ options.onShellError?.(err)
273
+ } else {
274
+ options.onError?.(err)
275
+ if (dest) dest.end()
276
+ }
277
+ },
278
+ )
279
+
280
+ // We call onShellReady once the first synchronous emit has landed.
281
+ // streamHtml above runs the shell synchronously before the first await in drain(),
282
+ // so we can schedule onShellReady right after the first microtask.
283
+ queueMicrotask(() => {
284
+ if (aborted || finished) return
285
+ shellReady = true
286
+ options.onShellReady?.()
287
+ })
288
+
289
+ return {
290
+ pipe<T extends NodeJS.WritableStream>(target: T): T {
291
+ dest = target
292
+ flushTo(target)
293
+ if (finished) target.end()
294
+ return target
295
+ },
296
+ abort(_reason?: unknown) {
297
+ aborted = true
298
+ state.closed = true
299
+ if (dest) dest.end()
300
+ },
301
+ }
302
+ }