@vidact/runtime 0.2.0-beta.0 → 0.2.0-beta.4

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 (244) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +1 -1
  3. package/dist/actions.d.ts +32 -30
  4. package/dist/actions.d.ts.map +1 -1
  5. package/dist/actions.js +251 -266
  6. package/dist/actions.js.map +1 -1
  7. package/dist/async.d.ts +20 -17
  8. package/dist/async.d.ts.map +1 -1
  9. package/dist/async.js +18 -17
  10. package/dist/async.js.map +1 -1
  11. package/dist/compiled/async-resource.d.ts +9 -6
  12. package/dist/compiled/async-resource.d.ts.map +1 -1
  13. package/dist/compiled/async-resource.js +69 -77
  14. package/dist/compiled/async-resource.js.map +1 -1
  15. package/dist/compiled/core.d.ts +136 -147
  16. package/dist/compiled/core.d.ts.map +1 -1
  17. package/dist/compiled/core.js +2669 -2985
  18. package/dist/compiled/core.js.map +1 -1
  19. package/dist/compiled/types.d.ts +44 -56
  20. package/dist/compiled/types.d.ts.map +1 -1
  21. package/dist/compiled/types.js +10 -6
  22. package/dist/compiled/types.js.map +1 -1
  23. package/dist/compiled/validation.d.ts +4 -1
  24. package/dist/compiled/validation.d.ts.map +1 -1
  25. package/dist/compiled/validation.js +17 -20
  26. package/dist/compiled/validation.js.map +1 -1
  27. package/dist/component-spread.d.ts +5 -3
  28. package/dist/component-spread.d.ts.map +1 -1
  29. package/dist/component-spread.js +49 -67
  30. package/dist/component-spread.js.map +1 -1
  31. package/dist/concurrent.d.ts +20 -18
  32. package/dist/concurrent.d.ts.map +1 -1
  33. package/dist/concurrent.js +191 -210
  34. package/dist/concurrent.js.map +1 -1
  35. package/dist/direct-dom.d.ts +17 -12
  36. package/dist/direct-dom.d.ts.map +1 -1
  37. package/dist/direct-dom.js +187 -221
  38. package/dist/direct-dom.js.map +1 -1
  39. package/dist/dom/attributes.d.ts +9 -6
  40. package/dist/dom/attributes.d.ts.map +1 -1
  41. package/dist/dom/attributes.js +65 -47
  42. package/dist/dom/attributes.js.map +1 -1
  43. package/dist/dom/events.d.ts +5 -2
  44. package/dist/dom/events.d.ts.map +1 -1
  45. package/dist/dom/events.js +53 -65
  46. package/dist/dom/events.js.map +1 -1
  47. package/dist/dom/forms.d.ts +9 -6
  48. package/dist/dom/forms.d.ts.map +1 -1
  49. package/dist/dom/forms.js +152 -198
  50. package/dist/dom/forms.js.map +1 -1
  51. package/dist/dom/intrinsic.d.ts +17 -15
  52. package/dist/dom/intrinsic.d.ts.map +1 -1
  53. package/dist/dom/intrinsic.js +48 -50
  54. package/dist/dom/intrinsic.js.map +1 -1
  55. package/dist/dom/namespace.d.ts +4 -1
  56. package/dist/dom/namespace.d.ts.map +1 -1
  57. package/dist/dom/namespace.js +25 -28
  58. package/dist/dom/namespace.js.map +1 -1
  59. package/dist/dom/namespaces.d.ts +11 -8
  60. package/dist/dom/namespaces.d.ts.map +1 -1
  61. package/dist/dom/namespaces.js +14 -12
  62. package/dist/dom/namespaces.js.map +1 -1
  63. package/dist/dom/properties.d.ts +18 -16
  64. package/dist/dom/properties.d.ts.map +1 -1
  65. package/dist/dom/properties.js +273 -296
  66. package/dist/dom/properties.js.map +1 -1
  67. package/dist/dom/style-properties.d.ts +4 -1
  68. package/dist/dom/style-properties.d.ts.map +1 -1
  69. package/dist/dom/style-properties.js +49 -45
  70. package/dist/dom/style-properties.js.map +1 -1
  71. package/dist/dom/styles.d.ts +5 -2
  72. package/dist/dom/styles.d.ts.map +1 -1
  73. package/dist/dom/styles.js +47 -67
  74. package/dist/dom/styles.js.map +1 -1
  75. package/dist/dom-forms.d.ts +4 -1
  76. package/dist/dom-forms.d.ts.map +1 -1
  77. package/dist/dom-forms.js +16 -13
  78. package/dist/dom-forms.js.map +1 -1
  79. package/dist/dom-styles.d.ts +4 -1
  80. package/dist/dom-styles.d.ts.map +1 -1
  81. package/dist/dom-styles.js +10 -7
  82. package/dist/dom-styles.js.map +1 -1
  83. package/dist/framework-hydrate.d.ts +20 -17
  84. package/dist/framework-hydrate.d.ts.map +1 -1
  85. package/dist/framework-hydrate.js +128 -146
  86. package/dist/framework-hydrate.js.map +1 -1
  87. package/dist/framework-protocol.d.ts +27 -25
  88. package/dist/framework-protocol.d.ts.map +1 -1
  89. package/dist/framework-protocol.js +173 -211
  90. package/dist/framework-protocol.js.map +1 -1
  91. package/dist/framework.d.ts +34 -32
  92. package/dist/framework.d.ts.map +1 -1
  93. package/dist/framework.js +74 -93
  94. package/dist/framework.js.map +1 -1
  95. package/dist/hydrate.d.ts +11 -8
  96. package/dist/hydrate.d.ts.map +1 -1
  97. package/dist/hydrate.js +13 -9
  98. package/dist/hydrate.js.map +1 -1
  99. package/dist/hydration-bridge.d.ts +61 -58
  100. package/dist/hydration-bridge.d.ts.map +1 -1
  101. package/dist/hydration-bridge.js +67 -66
  102. package/dist/hydration-bridge.js.map +1 -1
  103. package/dist/hydration.d.ts +31 -28
  104. package/dist/hydration.d.ts.map +1 -1
  105. package/dist/hydration.js +376 -462
  106. package/dist/hydration.js.map +1 -1
  107. package/dist/index.d.ts +13 -13
  108. package/dist/index.js +11 -13
  109. package/dist/indexed-list.d.ts +8 -5
  110. package/dist/indexed-list.d.ts.map +1 -1
  111. package/dist/indexed-list.js +14 -7
  112. package/dist/indexed-list.js.map +1 -1
  113. package/dist/jsx-runtime.d.ts +10 -8
  114. package/dist/jsx-runtime.d.ts.map +1 -1
  115. package/dist/jsx-runtime.js +28 -23
  116. package/dist/jsx-runtime.js.map +1 -1
  117. package/dist/keyed-list.d.ts +13 -14
  118. package/dist/keyed-list.d.ts.map +1 -1
  119. package/dist/keyed-list.js +132 -160
  120. package/dist/keyed-list.js.map +1 -1
  121. package/dist/metadata.d.ts +6 -3
  122. package/dist/metadata.d.ts.map +1 -1
  123. package/dist/metadata.js +131 -164
  124. package/dist/metadata.js.map +1 -1
  125. package/dist/nested-prop.d.ts +4 -2
  126. package/dist/nested-prop.d.ts.map +1 -1
  127. package/dist/nested-prop.js +17 -16
  128. package/dist/nested-prop.js.map +1 -1
  129. package/dist/profiling.d.ts +11 -8
  130. package/dist/profiling.d.ts.map +1 -1
  131. package/dist/profiling.js +17 -17
  132. package/dist/profiling.js.map +1 -1
  133. package/dist/protocol.d.ts +5 -2
  134. package/dist/protocol.d.ts.map +1 -1
  135. package/dist/protocol.js +8 -5
  136. package/dist/protocol.js.map +1 -1
  137. package/dist/raw-html.d.ts +5 -2
  138. package/dist/raw-html.d.ts.map +1 -1
  139. package/dist/raw-html.js +86 -146
  140. package/dist/raw-html.js.map +1 -1
  141. package/dist/ref.d.ts +8 -3
  142. package/dist/ref.d.ts.map +1 -1
  143. package/dist/ref.js +10 -2
  144. package/dist/ref.js.map +1 -1
  145. package/dist/renderable-protocol.d.ts +26 -8
  146. package/dist/renderable-protocol.d.ts.map +1 -1
  147. package/dist/renderable-protocol.js +30 -9
  148. package/dist/renderable-protocol.js.map +1 -1
  149. package/dist/renderable.d.ts +27 -22
  150. package/dist/renderable.d.ts.map +1 -1
  151. package/dist/renderable.js +141 -133
  152. package/dist/renderable.js.map +1 -1
  153. package/dist/retained-ui.d.ts +9 -6
  154. package/dist/retained-ui.d.ts.map +1 -1
  155. package/dist/retained-ui.js +12 -10
  156. package/dist/retained-ui.js.map +1 -1
  157. package/dist/root.d.ts +18 -15
  158. package/dist/root.d.ts.map +1 -1
  159. package/dist/root.js +73 -85
  160. package/dist/root.js.map +1 -1
  161. package/dist/scheduler.d.ts +10 -8
  162. package/dist/scheduler.d.ts.map +1 -1
  163. package/dist/scheduler.js +97 -105
  164. package/dist/scheduler.js.map +1 -1
  165. package/dist/server-framework.d.ts +45 -42
  166. package/dist/server-framework.d.ts.map +1 -1
  167. package/dist/server-framework.js +209 -217
  168. package/dist/server-framework.js.map +1 -1
  169. package/dist/server.d.ts +130 -128
  170. package/dist/server.d.ts.map +1 -1
  171. package/dist/server.js +592 -706
  172. package/dist/server.js.map +1 -1
  173. package/dist/shared/promise.d.ts +6 -3
  174. package/dist/shared/promise.d.ts.map +1 -1
  175. package/dist/shared/promise.js +21 -19
  176. package/dist/shared/promise.js.map +1 -1
  177. package/dist/source-mask.d.ts +9 -6
  178. package/dist/source-mask.d.ts.map +1 -1
  179. package/dist/source-mask.js +45 -63
  180. package/dist/source-mask.js.map +1 -1
  181. package/dist/spread.d.ts +5 -2
  182. package/dist/spread.d.ts.map +1 -1
  183. package/dist/spread.js +79 -99
  184. package/dist/spread.js.map +1 -1
  185. package/dist/state-slot.d.ts +18 -15
  186. package/dist/state-slot.d.ts.map +1 -1
  187. package/dist/state-slot.js +43 -42
  188. package/dist/state-slot.js.map +1 -1
  189. package/dist/testing.d.ts +9 -6
  190. package/dist/testing.d.ts.map +1 -1
  191. package/dist/testing.js +16 -12
  192. package/dist/testing.js.map +1 -1
  193. package/package.json +8 -7
  194. package/src/actions.ts +415 -0
  195. package/src/async.ts +78 -0
  196. package/src/compiled/async-resource.ts +113 -0
  197. package/src/compiled/core.ts +4026 -0
  198. package/src/compiled/types.ts +96 -0
  199. package/src/compiled/validation.ts +23 -0
  200. package/src/component-spread.ts +86 -0
  201. package/src/concurrent.ts +310 -0
  202. package/src/direct-dom.ts +365 -0
  203. package/src/dom/attributes.ts +55 -0
  204. package/src/dom/events.ts +93 -0
  205. package/src/dom/forms.ts +220 -0
  206. package/src/dom/intrinsic.ts +94 -0
  207. package/src/dom/namespace.ts +30 -0
  208. package/src/dom/namespaces.ts +14 -0
  209. package/src/dom/properties.ts +355 -0
  210. package/src/dom/style-properties.ts +46 -0
  211. package/src/dom/styles.ts +86 -0
  212. package/src/dom-forms.ts +23 -0
  213. package/src/dom-styles.ts +11 -0
  214. package/src/env.d.ts +6 -0
  215. package/src/framework-hydrate.ts +224 -0
  216. package/src/framework-protocol.ts +335 -0
  217. package/src/framework.ts +161 -0
  218. package/src/hydrate.ts +38 -0
  219. package/src/hydration-bridge.ts +183 -0
  220. package/src/hydration.ts +556 -0
  221. package/src/index.ts +92 -0
  222. package/src/indexed-list.ts +20 -0
  223. package/src/jsx-runtime.ts +55 -0
  224. package/src/keyed-list.ts +233 -0
  225. package/src/metadata.ts +212 -0
  226. package/src/nested-prop.ts +27 -0
  227. package/src/profiling.ts +43 -0
  228. package/src/protocol.ts +8 -0
  229. package/src/raw-html.ts +206 -0
  230. package/src/ref.ts +13 -0
  231. package/src/renderable-protocol.ts +63 -0
  232. package/src/renderable.ts +283 -0
  233. package/src/retained-ui.ts +19 -0
  234. package/src/root.ts +122 -0
  235. package/src/scheduler.ts +136 -0
  236. package/src/server-framework.ts +397 -0
  237. package/src/server.ts +1143 -0
  238. package/src/shared/promise.ts +28 -0
  239. package/src/source-mask.ts +83 -0
  240. package/src/spread.ts +131 -0
  241. package/src/state-slot.ts +76 -0
  242. package/src/testing.ts +27 -0
  243. package/dist/index.d.ts.map +0 -1
  244. package/dist/index.js.map +0 -1
package/src/root.ts ADDED
@@ -0,0 +1,122 @@
1
+ import { hydrateCompiled, mountCompiled, type MountCompiledOptions } from './compiled/core.ts'
2
+ import type { CompiledComponentResult } from './compiled/types.ts'
3
+
4
+ const DEV = typeof __VIDACT_DEV__ === 'undefined' || __VIDACT_DEV__
5
+
6
+ export interface CompiledRoot {
7
+ readonly mount: (application: () => CompiledComponentResult) => void
8
+ readonly replace: (application: () => CompiledComponentResult) => void
9
+ readonly unmount: () => void
10
+ }
11
+
12
+ export interface HotContext {
13
+ readonly data: Record<string, unknown>
14
+ readonly accept: () => void
15
+ readonly dispose: (callback: (data: Record<string, unknown>) => void) => void
16
+ readonly prune: (callback: () => void) => void
17
+ }
18
+
19
+ const HOT_ROOT = '__vidactHotRoot'
20
+
21
+ export function createRoot(host: ParentNode, options?: MountCompiledOptions): CompiledRoot {
22
+ let dispose: (() => void) | undefined
23
+ let terminal = false
24
+ return {
25
+ mount(application) {
26
+ if (terminal) throw new Error(DEV ? 'cannot mount an unmounted root' : 'V024')
27
+ if (dispose !== undefined) {
28
+ throw new Error(DEV ? 'compiled root already has a mounted application' : 'V025')
29
+ }
30
+ dispose = mountCompiled(application, host, options).dispose
31
+ },
32
+ replace(nextApplication) {
33
+ if (terminal) throw new Error(DEV ? 'cannot replace an unmounted root' : 'V030')
34
+ const previous = dispose
35
+ const next = mountCompiled(nextApplication, host, options).dispose
36
+ dispose = next
37
+ previous?.()
38
+ },
39
+ unmount() {
40
+ if (terminal) return
41
+ terminal = true
42
+ dispose?.()
43
+ dispose = undefined
44
+ },
45
+ }
46
+ }
47
+
48
+ export function hydrateRoot(
49
+ host: ParentNode,
50
+ application: () => CompiledComponentResult,
51
+ options?: MountCompiledOptions,
52
+ ): CompiledRoot {
53
+ let dispose: (() => void) | undefined = hydrateCompiled(application, host, options).dispose
54
+ let terminal = false
55
+ return {
56
+ mount() {
57
+ throw new Error(DEV ? 'hydration roots are mounted during creation' : 'V029')
58
+ },
59
+ replace(nextApplication) {
60
+ if (terminal) throw new Error(DEV ? 'cannot replace an unmounted root' : 'V030')
61
+ const previous = dispose
62
+ const next = mountCompiled(nextApplication, host, options).dispose
63
+ dispose = next
64
+ previous?.()
65
+ },
66
+ unmount() {
67
+ if (terminal) return
68
+ terminal = true
69
+ dispose?.()
70
+ dispose = undefined
71
+ },
72
+ }
73
+ }
74
+
75
+ export function mountHotRoot(
76
+ hot: HotContext,
77
+ host: ParentNode,
78
+ application: () => CompiledComponentResult,
79
+ options?: MountCompiledOptions,
80
+ ): CompiledRoot {
81
+ const previous = hot.data[HOT_ROOT]
82
+ const root = isCompiledRoot(previous) ? previous : createRoot(host, options)
83
+ if (isCompiledRoot(previous)) root.replace(application)
84
+ else root.mount(application)
85
+ registerHotRoot(hot, root)
86
+ return root
87
+ }
88
+
89
+ export function hydrateHotRoot(
90
+ hot: HotContext,
91
+ host: ParentNode,
92
+ application: () => CompiledComponentResult,
93
+ options?: MountCompiledOptions,
94
+ ): CompiledRoot {
95
+ const previous = hot.data[HOT_ROOT]
96
+ const root = isCompiledRoot(previous) ? previous : hydrateRoot(host, application, options)
97
+ if (isCompiledRoot(previous)) root.replace(application)
98
+ registerHotRoot(hot, root)
99
+ return root
100
+ }
101
+
102
+ function registerHotRoot(hot: HotContext, root: CompiledRoot): void {
103
+ hot.data[HOT_ROOT] = root
104
+ hot.accept()
105
+ hot.dispose((data) => {
106
+ data[HOT_ROOT] = root
107
+ })
108
+ hot.prune(() => {
109
+ if (hot.data[HOT_ROOT] !== root) return
110
+ delete hot.data[HOT_ROOT]
111
+ root.unmount()
112
+ })
113
+ }
114
+
115
+ function isCompiledRoot(value: unknown): value is CompiledRoot {
116
+ return (
117
+ typeof value === 'object' &&
118
+ value !== null &&
119
+ typeof Reflect.get(value, 'replace') === 'function' &&
120
+ typeof Reflect.get(value, 'unmount') === 'function'
121
+ )
122
+ }
@@ -0,0 +1,136 @@
1
+ type ScheduledTask = () => void
2
+ export type CancelScheduledTask = () => void
3
+
4
+ type DeferredTask = {
5
+ readonly run: ScheduledTask
6
+ canceled: boolean
7
+ }
8
+
9
+ let tasks: ScheduledTask[] = []
10
+ let deferredTasks: DeferredTask[] = []
11
+ let microtaskScheduled = false
12
+ let deferredTurnScheduled = false
13
+ let flushing = false
14
+ let actDepth = 0
15
+
16
+ export function scheduleTask(task: ScheduledTask): void {
17
+ tasks.push(task)
18
+ requestFlush()
19
+ }
20
+
21
+ export function scheduleDeferredTask(task: ScheduledTask): CancelScheduledTask {
22
+ const entry: DeferredTask = { run: task, canceled: false }
23
+ deferredTasks.push(entry)
24
+ requestDeferredTurn()
25
+ return () => {
26
+ entry.canceled = true
27
+ }
28
+ }
29
+
30
+ export function flushScheduledTasks(): number {
31
+ if (flushing) return 0
32
+ flushing = true
33
+ microtaskScheduled = false
34
+ let firstError: unknown
35
+ let failed = false
36
+ let count = 0
37
+ try {
38
+ const queued = tasks
39
+ const deferred = deferredTasks
40
+ tasks = []
41
+ deferredTasks = []
42
+ for (const task of queued) {
43
+ count += 1
44
+ try {
45
+ task()
46
+ } catch (error) {
47
+ if (!failed) {
48
+ firstError = error
49
+ failed = true
50
+ }
51
+ }
52
+ }
53
+ for (const task of deferred) {
54
+ if (task.canceled) continue
55
+ count += 1
56
+ try {
57
+ task.run()
58
+ } catch (error) {
59
+ if (!failed) {
60
+ firstError = error
61
+ failed = true
62
+ }
63
+ }
64
+ }
65
+ } finally {
66
+ flushing = false
67
+ requestFlush()
68
+ requestDeferredTurn()
69
+ }
70
+ if (failed) throw firstError
71
+ return count
72
+ }
73
+
74
+ export function hasScheduledTasks(): boolean {
75
+ return tasks.length > 0 || deferredTasks.some((task) => !task.canceled)
76
+ }
77
+
78
+ export function beginActScope(): void {
79
+ actDepth += 1
80
+ }
81
+
82
+ export function endActScope(): void {
83
+ actDepth -= 1
84
+ requestFlush()
85
+ requestDeferredTurn()
86
+ }
87
+
88
+ function requestFlush(): void {
89
+ if (tasks.length === 0 || microtaskScheduled || flushing || actDepth > 0) return
90
+ microtaskScheduled = true
91
+ queueMicrotask(() => {
92
+ microtaskScheduled = false
93
+ if (actDepth === 0) flushScheduledTasks()
94
+ })
95
+ }
96
+
97
+ function requestDeferredTurn(): void {
98
+ if (
99
+ deferredTasks.every((task) => task.canceled) ||
100
+ deferredTurnScheduled ||
101
+ flushing ||
102
+ actDepth > 0
103
+ ) {
104
+ return
105
+ }
106
+ deferredTurnScheduled = true
107
+ const channel = new MessageChannel()
108
+ channel.port1.addEventListener(
109
+ 'message',
110
+ () => {
111
+ channel.port1.close()
112
+ channel.port2.close()
113
+ deferredTurnScheduled = false
114
+ if (actDepth > 0 || flushing) {
115
+ requestDeferredTurn()
116
+ return
117
+ }
118
+ flushOneDeferredTask()
119
+ },
120
+ { once: true },
121
+ )
122
+ channel.port1.start()
123
+ channel.port2.postMessage(undefined)
124
+ }
125
+
126
+ function flushOneDeferredTask(): void {
127
+ let task: DeferredTask | undefined
128
+ while ((task = deferredTasks.shift()) !== undefined && task.canceled) {}
129
+ if (task === undefined) return
130
+ try {
131
+ task.run()
132
+ } finally {
133
+ requestFlush()
134
+ requestDeferredTurn()
135
+ }
136
+ }
@@ -0,0 +1,397 @@
1
+ import type { CompiledComponentResult } from './compiled/types.ts'
2
+ import {
3
+ decodeFrameworkValue,
4
+ encodeFrameworkValue,
5
+ type ClientModuleManifest,
6
+ type ClientReference,
7
+ type FrameworkValue,
8
+ } from './framework-protocol.ts'
9
+ import {
10
+ createServerHydrationBoundary,
11
+ createServerFrameworkRenderContext,
12
+ renderToString,
13
+ withServerFrameworkRenderContext,
14
+ type ServerChild,
15
+ type ServerFrameworkRenderContext,
16
+ type ServerNode,
17
+ type ServerRenderOptions,
18
+ } from './server.ts'
19
+ import { abortReason, withAbort } from './shared/promise.ts'
20
+
21
+ export interface FrameworkRenderOptions extends ServerRenderOptions {
22
+ readonly signal?: AbortSignal
23
+ readonly progressiveChunkSize?: number
24
+ readonly onError?: (error: unknown) => void
25
+ }
26
+
27
+ export interface PipeableRenderOptions extends FrameworkRenderOptions {
28
+ readonly onAllReady?: () => void
29
+ readonly onShellError?: (error: unknown) => void
30
+ readonly onShellReady?: () => void
31
+ }
32
+
33
+ export interface PipeDestination {
34
+ readonly write: (chunk: Uint8Array) => boolean
35
+ readonly end: () => void
36
+ readonly destroy?: (error: unknown) => void
37
+ readonly once?: (event: 'drain', listener: () => void) => void
38
+ readonly off?: (event: 'drain', listener: () => void) => void
39
+ }
40
+
41
+ export interface PipeableStream {
42
+ readonly abort: (reason?: unknown) => void
43
+ readonly pipe: (destination: PipeDestination) => void
44
+ }
45
+
46
+ export type VidactReadableStream = ReadableStream<Uint8Array> & {
47
+ readonly allReady: Promise<void>
48
+ }
49
+
50
+ export interface PrerenderResult<Prelude> {
51
+ readonly prelude: Prelude
52
+ readonly postponed: string | null
53
+ }
54
+
55
+ export interface ServerComponentPayload {
56
+ readonly kind: 'server-component'
57
+ readonly html: string
58
+ readonly model: FrameworkValue
59
+ }
60
+
61
+ export interface ClientBoundaryOptions {
62
+ readonly clientManifest: ClientModuleManifest
63
+ readonly hostProps?: Readonly<Record<string, unknown>>
64
+ }
65
+
66
+ export function createClientBoundary(
67
+ reference: ClientReference,
68
+ props: FrameworkValue,
69
+ serverContent: ServerChild | CompiledComponentResult,
70
+ options: ClientBoundaryOptions,
71
+ ): ServerNode & CompiledComponentResult {
72
+ const payload = encodeFrameworkValue({ reference, props }, options.clientManifest)
73
+ return createServerHydrationBoundary(serverContent as ServerChild, {
74
+ ...options.hostProps,
75
+ 'data-vidact-client-boundary': true,
76
+ 'data-vidact-client-payload': payload,
77
+ }) as ServerNode & CompiledComponentResult
78
+ }
79
+
80
+ export function renderToReadableStream(
81
+ value: ServerChild | (() => ServerChild),
82
+ options: FrameworkRenderOptions = {},
83
+ ): Promise<VidactReadableStream> {
84
+ const controller = linkedAbortController(options.signal)
85
+ const html = renderFrameworkHtml(value, options, controller.signal)
86
+ return Promise.resolve(readableFromText(html, options.progressiveChunkSize, controller))
87
+ }
88
+
89
+ export function renderToPipeableStream(
90
+ value: ServerChild | (() => ServerChild),
91
+ options: PipeableRenderOptions = {},
92
+ ): PipeableStream {
93
+ const controller = linkedAbortController(options.signal)
94
+ const html = renderFrameworkHtml(value, options, controller.signal, options)
95
+ // A caller may create a stream for lifecycle callbacks and never pipe it. Mark the
96
+ // render rejection as observed while retaining the original promise for pipe().
97
+ void html.catch(() => undefined)
98
+ let piped = false
99
+ return {
100
+ abort(reason) {
101
+ controller.abort(reason)
102
+ },
103
+ pipe(destination) {
104
+ if (piped) throw new Error('a Vidact pipeable stream can only be piped once')
105
+ piped = true
106
+ void pipeText(html, destination, options.progressiveChunkSize, controller.signal).catch(
107
+ (error: unknown) => destination.destroy?.(error),
108
+ )
109
+ },
110
+ }
111
+ }
112
+
113
+ export async function prerender(
114
+ value: ServerChild | (() => ServerChild),
115
+ options: FrameworkRenderOptions = {},
116
+ ): Promise<PrerenderResult<ReadableStream<Uint8Array>>> {
117
+ const controller = linkedAbortController(options.signal)
118
+ const result = await prerenderText(value, options, controller.signal)
119
+ return {
120
+ prelude: readableFromText(
121
+ Promise.resolve(result.prelude),
122
+ options.progressiveChunkSize,
123
+ controller,
124
+ ),
125
+ postponed: result.postponed,
126
+ }
127
+ }
128
+
129
+ export async function prerenderToNodeStream(
130
+ value: ServerChild | (() => ServerChild),
131
+ options: FrameworkRenderOptions = {},
132
+ ): Promise<PrerenderResult<AsyncIterable<Uint8Array>>> {
133
+ const controller = linkedAbortController(options.signal)
134
+ const result = await prerenderText(value, options, controller.signal)
135
+ return {
136
+ prelude: textIterable(result.prelude, options.progressiveChunkSize, controller.signal),
137
+ postponed: result.postponed,
138
+ }
139
+ }
140
+
141
+ export function resume(
142
+ postponed: string,
143
+ options: FrameworkRenderOptions = {},
144
+ ): Promise<VidactReadableStream> {
145
+ const controller = linkedAbortController(options.signal)
146
+ const html = Promise.resolve(readContinuation(postponed))
147
+ return Promise.resolve(readableFromText(html, options.progressiveChunkSize, controller))
148
+ }
149
+
150
+ export function resumeToPipeableStream(
151
+ postponed: string,
152
+ options: PipeableRenderOptions = {},
153
+ ): PipeableStream {
154
+ const controller = linkedAbortController(options.signal)
155
+ const html = Promise.resolve(readContinuation(postponed))
156
+ let piped = false
157
+ queueMicrotask(() => options.onShellReady?.())
158
+ void html.then(
159
+ () => options.onAllReady?.(),
160
+ (error: unknown) => options.onShellError?.(error),
161
+ )
162
+ return {
163
+ abort(reason) {
164
+ controller.abort(reason)
165
+ },
166
+ pipe(destination) {
167
+ if (piped) throw new Error('a Vidact pipeable stream can only be piped once')
168
+ piped = true
169
+ void pipeText(html, destination, options.progressiveChunkSize, controller.signal).catch(
170
+ (error: unknown) => destination.destroy?.(error),
171
+ )
172
+ },
173
+ }
174
+ }
175
+
176
+ export async function renderServerComponentPayload(
177
+ value: ServerChild | (() => ServerChild),
178
+ model: FrameworkValue,
179
+ options: FrameworkRenderOptions & { readonly clientManifest?: ClientModuleManifest } = {},
180
+ ): Promise<string> {
181
+ const controller = linkedAbortController(options.signal)
182
+ const html = await renderFrameworkHtml(value, options, controller.signal)
183
+ return encodeFrameworkValue({ kind: 'server-component', html, model }, options.clientManifest)
184
+ }
185
+
186
+ export function decodeServerComponentPayload(
187
+ payload: string,
188
+ clientManifest?: ClientModuleManifest,
189
+ ): ServerComponentPayload {
190
+ const value = decodeFrameworkValue(payload, clientManifest)
191
+ if (
192
+ !isRecord(value) ||
193
+ value.kind !== 'server-component' ||
194
+ typeof value.html !== 'string' ||
195
+ !Object.hasOwn(value, 'model')
196
+ ) {
197
+ throw new TypeError('invalid Vidact Server Component payload')
198
+ }
199
+ return { kind: 'server-component', html: value.html, model: value.model as FrameworkValue }
200
+ }
201
+
202
+ async function renderFrameworkHtml(
203
+ value: ServerChild | (() => ServerChild),
204
+ options: FrameworkRenderOptions,
205
+ signal: AbortSignal,
206
+ callbacks: PipeableRenderOptions = {},
207
+ ): Promise<string> {
208
+ const context = createServerFrameworkRenderContext(signal)
209
+ let shellReported = false
210
+ for (let pass = 0; pass < 100; pass += 1) {
211
+ throwIfAborted(signal)
212
+ let html: string
213
+ try {
214
+ html = renderFrameworkPass(value, options, context)
215
+ } catch (error) {
216
+ options.onError?.(error)
217
+ if (!shellReported) callbacks.onShellError?.(error)
218
+ throw error
219
+ }
220
+ if (!shellReported) {
221
+ shellReported = true
222
+ callbacks.onShellReady?.()
223
+ }
224
+ if (context.pending.size === 0) {
225
+ callbacks.onAllReady?.()
226
+ return html
227
+ }
228
+ // oxlint-disable-next-line eslint/no-await-in-loop -- Each pass discovers the next resource frontier.
229
+ await waitForResources(context.pending, signal)
230
+ }
231
+ throw new Error('Vidact framework render did not stabilize after 100 resource passes')
232
+ }
233
+
234
+ async function prerenderText(
235
+ value: ServerChild | (() => ServerChild),
236
+ options: FrameworkRenderOptions,
237
+ signal: AbortSignal,
238
+ ): Promise<{ readonly prelude: string; readonly postponed: string | null }> {
239
+ const context = createServerFrameworkRenderContext(signal)
240
+ const shell = renderFrameworkPass(value, options, context)
241
+ if (context.pending.size === 0) return { prelude: shell, postponed: null }
242
+ await waitForResources(context.pending, signal)
243
+ const final = await settleFrameworkContext(value, options, context)
244
+ return {
245
+ prelude: shell,
246
+ postponed: encodeFrameworkValue({ kind: 'continuation', html: final }),
247
+ }
248
+ }
249
+
250
+ async function settleFrameworkContext(
251
+ value: ServerChild | (() => ServerChild),
252
+ options: FrameworkRenderOptions,
253
+ context: ServerFrameworkRenderContext,
254
+ ): Promise<string> {
255
+ for (let pass = 0; pass < 100; pass += 1) {
256
+ throwIfAborted(context.signal)
257
+ const html = renderFrameworkPass(value, options, context)
258
+ if (context.pending.size === 0) return html
259
+ // oxlint-disable-next-line eslint/no-await-in-loop -- Each pass discovers the next resource frontier.
260
+ await waitForResources(context.pending, context.signal)
261
+ }
262
+ throw new Error('Vidact prerender did not stabilize after 100 resource passes')
263
+ }
264
+
265
+ function renderFrameworkPass(
266
+ value: ServerChild | (() => ServerChild),
267
+ options: ServerRenderOptions,
268
+ context: ServerFrameworkRenderContext,
269
+ ): string {
270
+ return withServerFrameworkRenderContext(context, () => renderToString(value, options))
271
+ }
272
+
273
+ function readContinuation(postponed: string): string {
274
+ const value = decodeFrameworkValue(postponed)
275
+ if (!isRecord(value) || value.kind !== 'continuation' || typeof value.html !== 'string') {
276
+ throw new TypeError('invalid Vidact continuation payload')
277
+ }
278
+ return value.html
279
+ }
280
+
281
+ function readableFromText(
282
+ text: Promise<string>,
283
+ requestedChunkSize: number | undefined,
284
+ abortController: AbortController,
285
+ ): VidactReadableStream {
286
+ const chunkSize = normalizeChunkSize(requestedChunkSize)
287
+ const encoder = new TextEncoder()
288
+ let content: Uint8Array | undefined
289
+ let offset = 0
290
+ const allReady = text.then(() => undefined)
291
+ const stream = new ReadableStream<Uint8Array>({
292
+ async pull(controller) {
293
+ try {
294
+ throwIfAborted(abortController.signal)
295
+ content ??= encoder.encode(await text)
296
+ if (offset >= content.length) {
297
+ controller.close()
298
+ return
299
+ }
300
+ const next = content.slice(offset, offset + chunkSize)
301
+ offset += next.length
302
+ controller.enqueue(next)
303
+ } catch (error) {
304
+ controller.error(error)
305
+ }
306
+ },
307
+ cancel(reason) {
308
+ abortController.abort(reason)
309
+ },
310
+ }) as VidactReadableStream
311
+ Object.defineProperty(stream, 'allReady', { value: allReady })
312
+ return stream
313
+ }
314
+
315
+ async function pipeText(
316
+ text: Promise<string>,
317
+ destination: PipeDestination,
318
+ requestedChunkSize: number | undefined,
319
+ signal: AbortSignal,
320
+ ): Promise<void> {
321
+ const content = new TextEncoder().encode(await text)
322
+ const chunkSize = normalizeChunkSize(requestedChunkSize)
323
+ for (let offset = 0; offset < content.length; offset += chunkSize) {
324
+ throwIfAborted(signal)
325
+ if (!destination.write(content.slice(offset, offset + chunkSize))) {
326
+ // oxlint-disable-next-line eslint/no-await-in-loop -- Stream chunks must honor destination backpressure in order.
327
+ await waitForDrain(destination, signal)
328
+ }
329
+ }
330
+ destination.end()
331
+ }
332
+
333
+ async function* textIterable(
334
+ text: string,
335
+ requestedChunkSize: number | undefined,
336
+ signal: AbortSignal,
337
+ ): AsyncIterable<Uint8Array> {
338
+ const content = new TextEncoder().encode(text)
339
+ const chunkSize = normalizeChunkSize(requestedChunkSize)
340
+ for (let offset = 0; offset < content.length; offset += chunkSize) {
341
+ throwIfAborted(signal)
342
+ yield content.slice(offset, offset + chunkSize)
343
+ }
344
+ }
345
+
346
+ function waitForResources(
347
+ resources: Set<PromiseLike<unknown>>,
348
+ signal: AbortSignal,
349
+ ): Promise<void> {
350
+ return withAbort(
351
+ Promise.allSettled(resources).then(() => undefined),
352
+ signal,
353
+ )
354
+ }
355
+
356
+ function waitForDrain(destination: PipeDestination, signal: AbortSignal): Promise<void> {
357
+ if (destination.once === undefined) return Promise.resolve()
358
+ return new Promise((resolve, reject) => {
359
+ if (signal.aborted) {
360
+ reject(abortReason(signal))
361
+ return
362
+ }
363
+ const onDrain = (): void => {
364
+ signal.removeEventListener('abort', onAbort)
365
+ resolve()
366
+ }
367
+ const onAbort = (): void => {
368
+ destination.off?.('drain', onDrain)
369
+ reject(abortReason(signal))
370
+ }
371
+ destination.once?.('drain', onDrain)
372
+ signal.addEventListener('abort', onAbort, { once: true })
373
+ })
374
+ }
375
+
376
+ function linkedAbortController(signal?: AbortSignal): AbortController {
377
+ const controller = new AbortController()
378
+ if (signal?.aborted) controller.abort(signal.reason)
379
+ else signal?.addEventListener('abort', () => controller.abort(signal.reason), { once: true })
380
+ return controller
381
+ }
382
+
383
+ function normalizeChunkSize(value: number | undefined): number {
384
+ if (value === undefined) return 16_384
385
+ if (!Number.isSafeInteger(value) || value <= 0) {
386
+ throw new RangeError('progressiveChunkSize must be a positive safe integer')
387
+ }
388
+ return value
389
+ }
390
+
391
+ function throwIfAborted(signal: AbortSignal): void {
392
+ if (signal.aborted) throw abortReason(signal)
393
+ }
394
+
395
+ function isRecord(value: unknown): value is Record<string, FrameworkValue> {
396
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
397
+ }