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

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 +52 -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 +2672 -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 +70 -58
  100. package/dist/hydration-bridge.d.ts.map +1 -1
  101. package/dist/hydration-bridge.js +77 -66
  102. package/dist/hydration-bridge.js.map +1 -1
  103. package/dist/hydration.d.ts +54 -28
  104. package/dist/hydration.d.ts.map +1 -1
  105. package/dist/hydration.js +450 -463
  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 +603 -710
  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 +4041 -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 +196 -0
  220. package/src/hydration.ts +642 -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 +1160 -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
@@ -0,0 +1,642 @@
1
+ import {
2
+ HYDRATION_PREFIX,
3
+ HydrationMismatch,
4
+ installHydrationOperations,
5
+ type HydrationRange,
6
+ } from './hydration-bridge.ts'
7
+
8
+ interface HydrationState {
9
+ readonly host: ParentNode
10
+ readonly root: HydrationRange
11
+ readonly components: readonly HydrationRange[]
12
+ readonly claimedElements: WeakSet<Element>
13
+ readonly elementComponents: WeakMap<Element, Comment>
14
+ readonly elements: readonly Element[]
15
+ readonly cursors: WeakMap<Node, Node | null>
16
+ readonly slots: WeakMap<Node, Comment[]>
17
+ componentIndex: number
18
+ claimedElementCount: number
19
+ pendingStructuralParents: number
20
+ activeComponentStart: Comment | undefined
21
+ }
22
+
23
+ let activeHydration: HydrationState | undefined
24
+ let activeInsertionPoint: readonly [parent: Node, before: Node] | undefined
25
+ const hydrationFragments = new WeakMap<DocumentFragment, readonly unknown[]>()
26
+
27
+ export function beginHydration(host: ParentNode): () => void {
28
+ if (activeHydration !== undefined) {
29
+ throw new Error('a Vidact hydration is already active')
30
+ }
31
+ const ranges = scanRanges(host)
32
+ const root = ranges.roots[0]
33
+ if (root === undefined || ranges.roots.length !== 1) {
34
+ throw mismatch(`expected exactly one ${HYDRATION_PREFIX} root marker range`)
35
+ }
36
+ activeHydration = {
37
+ host,
38
+ root,
39
+ components: ranges.components,
40
+ claimedElements: new WeakSet(),
41
+ elementComponents: collectElementComponents(host),
42
+ elements: collectPostorderElements(root),
43
+ cursors: new WeakMap([[host as Node, root[0].nextSibling]]),
44
+ slots: new WeakMap(),
45
+ componentIndex: 0,
46
+ claimedElementCount: 0,
47
+ pendingStructuralParents: 0,
48
+ activeComponentStart: undefined,
49
+ }
50
+ return () => {
51
+ activeHydration = undefined
52
+ }
53
+ }
54
+
55
+ export function finishHydration(): void {
56
+ const state = requireHydration()
57
+ if (state.componentIndex !== state.components.length) {
58
+ throw mismatch(
59
+ `claimed ${state.componentIndex} of ${state.components.length} server component ranges`,
60
+ )
61
+ }
62
+ if (state.claimedElementCount !== state.elements.length) {
63
+ throw mismatch(
64
+ `claimed ${state.claimedElementCount} of ${state.elements.length} server elements`,
65
+ )
66
+ }
67
+ const rootCursor = cursor(state, state.host as Node)
68
+ if (rootCursor !== state.root[1]) {
69
+ throw mismatch(
70
+ `compiled root did not consume the complete server root range; stopped at ${JSON.stringify(markerValue(rootCursor as Node) ?? rootCursor?.nodeName ?? null)}`,
71
+ )
72
+ }
73
+ }
74
+
75
+ export function hydrationRootMarkers(): HydrationRange {
76
+ return requireHydration().root
77
+ }
78
+
79
+ export function isHydrating(): boolean {
80
+ return activeHydration !== undefined
81
+ }
82
+
83
+ export function createHydrationFragment(children: readonly unknown[]): DocumentFragment {
84
+ const fragment = document.createDocumentFragment()
85
+ hydrationFragments.set(fragment, children)
86
+ return fragment
87
+ }
88
+
89
+ export function hydrationFragmentChildren(
90
+ fragment: DocumentFragment,
91
+ ): readonly unknown[] | undefined {
92
+ return hydrationFragments.get(fragment)
93
+ }
94
+
95
+ export function isHydrationMismatch(error: unknown): error is HydrationMismatch {
96
+ return error instanceof HydrationMismatch
97
+ }
98
+
99
+ export function claimHydrationElement(
100
+ localName: string,
101
+ namespace: string | null,
102
+ matchesExpected?: (element: Element) => boolean,
103
+ ): Element | undefined {
104
+ const state = activeHydration
105
+ if (state === undefined) return undefined
106
+ const candidates = state.elements.filter(
107
+ (candidate) =>
108
+ !state.claimedElements.has(candidate) &&
109
+ (state.activeComponentStart === undefined ||
110
+ state.elementComponents.get(candidate) === state.activeComponentStart) &&
111
+ candidate.localName === localName &&
112
+ candidate.namespaceURI === namespace,
113
+ )
114
+ const expectedMatches =
115
+ matchesExpected === undefined ? candidates : candidates.filter(matchesExpected)
116
+ const matches = expectedMatches.length === 0 ? candidates : expectedMatches
117
+ const element =
118
+ state.pendingStructuralParents > 0
119
+ ? (matches.find(containsArrayMarker) ?? matches[0])
120
+ : matches[0]
121
+ if (element === undefined) {
122
+ throw mismatch(
123
+ state.pendingStructuralParents > 0
124
+ ? `missing server structural parent <${localName}>`
125
+ : `missing server element <${localName}>`,
126
+ )
127
+ }
128
+ if (state.pendingStructuralParents > 0) state.pendingStructuralParents = 0
129
+ state.claimedElements.add(element)
130
+ state.claimedElementCount += 1
131
+ state.cursors.set(element, initialCursor(element))
132
+ return element
133
+ }
134
+
135
+ /**
136
+ * Called by a compiled list right before its container element is claimed. Lists are the
137
+ * only structural bindings whose server markup carries an `a` marker, so they are the
138
+ * only ones that may prefer a candidate containing one — for a conditional, Suspense or
139
+ * any other structural child that bias would pick an unrelated element that happens to
140
+ * hold a list (see `claimHydrationElement`).
141
+ */
142
+ export function noteHydrationStructuralParent(): void {
143
+ if (activeHydration !== undefined) activeHydration.pendingStructuralParents += 1
144
+ }
145
+
146
+ export function claimHydrationComponentRange(): HydrationRange | undefined {
147
+ const state = activeHydration
148
+ if (state === undefined) return undefined
149
+ const range = state.components[state.componentIndex]
150
+ if (range === undefined) throw mismatch('missing server component marker range')
151
+ state.componentIndex += 1
152
+ const parent = range[0].parentNode
153
+ if (parent === null || range[1].parentNode !== parent) {
154
+ throw mismatch('server component marker range is detached')
155
+ }
156
+ return range
157
+ }
158
+
159
+ export function withHydrationComponentRange<Result>(
160
+ range: HydrationRange,
161
+ operation: () => Result,
162
+ ): Result {
163
+ const state = activeHydration
164
+ if (state === undefined) return operation()
165
+ const previous = state.activeComponentStart
166
+ state.activeComponentStart = range[0]
167
+ try {
168
+ return operation()
169
+ } finally {
170
+ state.activeComponentStart = previous
171
+ }
172
+ }
173
+
174
+ export function withHydrationCursor<Result>(
175
+ parent: Node,
176
+ value: Node | null,
177
+ operation: () => Result,
178
+ ): Result {
179
+ const state = activeHydration
180
+ if (state === undefined) return operation()
181
+ const hadCursor = state.cursors.has(parent)
182
+ const previous = state.cursors.get(parent) ?? null
183
+ state.cursors.set(parent, value)
184
+ try {
185
+ return operation()
186
+ } finally {
187
+ if (hadCursor) state.cursors.set(parent, previous)
188
+ else state.cursors.delete(parent)
189
+ }
190
+ }
191
+
192
+ export function claimHydrationNode(parent: Node, node: Node): boolean {
193
+ const state = activeHydration
194
+ if (state === undefined) return false
195
+ enterHydrationSlot(state, parent)
196
+ const next = cursor(state, parent)
197
+ if (next !== node) {
198
+ throw mismatch(`expected existing ${node.nodeName} at the current hydration position`)
199
+ }
200
+ setHydrationCursor(state, parent, node.nextSibling)
201
+ return true
202
+ }
203
+
204
+ export function claimHydrationComponentMount(parent: Node, start: Comment, end: Comment): boolean {
205
+ const state = activeHydration
206
+ if (state === undefined) return false
207
+ enterHydrationSlot(state, parent)
208
+ const current = cursor(state, parent)
209
+ if (
210
+ (current !== start && current !== end) ||
211
+ start.parentNode !== parent ||
212
+ end.parentNode !== parent
213
+ ) {
214
+ throw mismatch('server component range does not match the compiled child position')
215
+ }
216
+ setHydrationCursor(state, parent, end.nextSibling)
217
+ return true
218
+ }
219
+
220
+ export function claimHydrationText(parent: Node, expected: string): Text | null | undefined {
221
+ const state = activeHydration
222
+ if (state === undefined) return undefined
223
+ enterHydrationSlot(state, parent)
224
+ return claimTextPrefix(state, parent, expected)
225
+ }
226
+
227
+ /**
228
+ * Claims the text for a scalar binding together with a range it can later swap an
229
+ * element or collection into. The server emits no markers around text, so the binding
230
+ * borrows the enclosing child-slot markers when the slot holds exactly this text — the
231
+ * common case, and no DOM is touched. A text node shared with adjacent scalars (one
232
+ * parsed node for several array items) gets fresh anchor comments instead.
233
+ */
234
+ export function claimHydrationTextRange(
235
+ parent: Node,
236
+ expected: string,
237
+ ): readonly [start: Comment, end: Comment, text: Text | null] | undefined {
238
+ const state = activeHydration
239
+ if (state === undefined) return undefined
240
+ enterHydrationSlot(state, parent)
241
+ const at = cursor(state, parent)
242
+ const slotStart = at === null ? parent.lastChild : at.previousSibling
243
+ const text = claimTextPrefix(state, parent, expected)
244
+ const slotEnd = text === null ? at : text.nextSibling
245
+ if (
246
+ isMarker(slotStart, `${HYDRATION_PREFIX}:b`) &&
247
+ isMarker(slotEnd, `/${HYDRATION_PREFIX}:b`) &&
248
+ findClosingSibling(slotStart, `${HYDRATION_PREFIX}:b`) === slotEnd
249
+ ) {
250
+ return [slotStart, slotEnd, text]
251
+ }
252
+ const start = document.createComment('')
253
+ const end = document.createComment('')
254
+ const reference = text ?? at
255
+ parent.insertBefore(start, reference)
256
+ parent.insertBefore(end, text === null ? reference : text.nextSibling)
257
+ return [start, end, text]
258
+ }
259
+
260
+ /**
261
+ * The server renders a scalar as bare text, and nothing at all for an empty one. Adjacent
262
+ * scalars in one collection parse into a single text node, so a claim takes only its own
263
+ * prefix and splits the rest off for the next claim.
264
+ */
265
+ function claimTextPrefix(state: HydrationState, parent: Node, expected: string): Text | null {
266
+ const node = cursor(state, parent)
267
+ if (expected === '') {
268
+ if (node instanceof Text && node.data === '') {
269
+ setHydrationCursor(state, parent, node.nextSibling)
270
+ return node
271
+ }
272
+ // Nothing to consume, but closing slot markers at the cursor still need passing.
273
+ setHydrationCursor(state, parent, node)
274
+ return null
275
+ }
276
+ if (!(node instanceof Text)) {
277
+ throw mismatch(
278
+ `expected server text ${JSON.stringify(expected)} in ${describeNode(parent)}; found ${describeNode(node)}`,
279
+ )
280
+ }
281
+ if (!node.data.startsWith(expected)) {
282
+ throw mismatch(
283
+ `server text ${JSON.stringify(node.data)} does not match ${JSON.stringify(expected)}`,
284
+ )
285
+ }
286
+ if (node.data.length > expected.length) node.splitText(expected.length)
287
+ setHydrationCursor(state, parent, node.nextSibling)
288
+ return node
289
+ }
290
+
291
+ export function claimHydrationArrayRange(parent: Node): HydrationRange | undefined {
292
+ const state = activeHydration
293
+ if (state === undefined) return undefined
294
+ enterHydrationSlot(state, parent)
295
+ const start = cursor(state, parent)
296
+ if (!isMarker(start, `${HYDRATION_PREFIX}:a`)) {
297
+ throw mismatch(
298
+ `expected a ${HYDRATION_PREFIX} array marker in ${describeNode(parent)}; found ${JSON.stringify(markerValue(start as Node) ?? start?.nodeName ?? null)}`,
299
+ )
300
+ }
301
+ const end = findClosingSibling(start, `${HYDRATION_PREFIX}:a`)
302
+ state.cursors.set(parent, start.nextSibling)
303
+ return [start, end]
304
+ }
305
+
306
+ export function finishHydrationArrayRange(parent: Node, end: Comment): void {
307
+ const state = requireHydration()
308
+ if (cursor(state, parent) !== end) {
309
+ throw mismatch('compiled collection did not consume its complete server array range')
310
+ }
311
+ setHydrationCursor(state, parent, end.nextSibling)
312
+ }
313
+
314
+ export function claimHydrationSlotRange(parent: Node): HydrationRange | undefined {
315
+ const state = activeHydration
316
+ if (state === undefined) return undefined
317
+ const start = cursor(state, parent)
318
+ if (!isMarker(start, `${HYDRATION_PREFIX}:b`)) {
319
+ throw mismatch(
320
+ `expected a ${HYDRATION_PREFIX} child-slot marker in ${describeNode(parent)}; found ${JSON.stringify(markerValue(start as Node) ?? start?.nodeName ?? null)} after ${JSON.stringify(markerValue(start?.previousSibling as Node) ?? start?.previousSibling?.nodeName ?? null)}; insertion=${JSON.stringify(markerValue(activeInsertionPoint?.[1] as Node) ?? activeInsertionPoint?.[1]?.nodeName ?? null)}`,
321
+ )
322
+ }
323
+ const end = findClosingSibling(start, `${HYDRATION_PREFIX}:b`)
324
+ const slots = state.slots.get(parent) ?? []
325
+ slots.push(end)
326
+ state.slots.set(parent, slots)
327
+ state.cursors.set(parent, start.nextSibling)
328
+ return [start, end]
329
+ }
330
+
331
+ export function borrowHydrationSlotRange(
332
+ parent: Node,
333
+ current: boolean,
334
+ ): HydrationRange | undefined {
335
+ const state = activeHydration
336
+ if (state === undefined) return undefined
337
+ const start = (
338
+ current ? cursor(state, parent) : cursor(state, parent)?.previousSibling
339
+ ) as Node | null
340
+ if (!isMarker(start, `${HYDRATION_PREFIX}:b`)) return undefined
341
+ if (
342
+ !current &&
343
+ activeInsertionPoint?.[0] === parent &&
344
+ isMarker(activeInsertionPoint[1], `/${HYDRATION_PREFIX}:b`)
345
+ ) {
346
+ return [start, activeInsertionPoint[1]]
347
+ }
348
+ return [start, findClosingSibling(start, `${HYDRATION_PREFIX}:b`)]
349
+ }
350
+
351
+ /**
352
+ * Claims everything between two markers as-is, without hydrating into it. A Suspense
353
+ * boundary uses this when the server rendered its content but the client cannot
354
+ * produce it yet (a lazy chunk still loading): the server DOM stays on screen as a
355
+ * dehydrated boundary and the client render replaces it once the resource resolves.
356
+ * The elements and component ranges inside are marked claimed so `finishHydration`'s
357
+ * accounting still balances.
358
+ */
359
+ export function skipHydrationRange(start: Comment, end: Comment): void {
360
+ const state = requireHydration()
361
+ const parent = start.parentNode
362
+ if (parent === null || end.parentNode !== parent) throw mismatch('detached server marker range')
363
+ for (const element of collectPostorderElements([start, end])) {
364
+ if (state.claimedElements.has(element)) continue
365
+ state.claimedElements.add(element)
366
+ state.claimedElementCount += 1
367
+ }
368
+ // Component ranges are sorted by document position and every earlier one is already
369
+ // claimed, so the ones inside this range sit contiguously at the current index.
370
+ while (state.componentIndex < state.components.length) {
371
+ const componentStart = state.components[state.componentIndex]![0]
372
+ const inside =
373
+ (start.compareDocumentPosition(componentStart) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0 &&
374
+ (componentStart.compareDocumentPosition(end) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0
375
+ if (!inside) break
376
+ state.componentIndex += 1
377
+ }
378
+ state.pendingStructuralParents = 0
379
+ setHydrationCursor(state, parent, end)
380
+ }
381
+
382
+ export function claimHydrationSuspenseFallback(parent: Node): Comment | undefined {
383
+ const state = activeHydration
384
+ if (state === undefined) return undefined
385
+ const marker = cursor(state, parent)
386
+ if (!isMarker(marker, `${HYDRATION_PREFIX}:p`)) return undefined
387
+ setHydrationCursor(state, parent, marker.nextSibling)
388
+ return marker
389
+ }
390
+
391
+ export function withoutHydration<Result>(operation: () => Result): Result {
392
+ const previous = activeHydration
393
+ activeHydration = undefined
394
+ try {
395
+ return operation()
396
+ } finally {
397
+ activeHydration = previous
398
+ }
399
+ }
400
+
401
+ export function withHydrationInsertion<Result>(
402
+ parent: Node,
403
+ before: Node,
404
+ operation: () => Result,
405
+ ): Result {
406
+ if (activeHydration === undefined) return operation()
407
+ const previous = activeInsertionPoint
408
+ activeInsertionPoint = [parent, before]
409
+ try {
410
+ return operation()
411
+ } finally {
412
+ activeInsertionPoint = previous
413
+ }
414
+ }
415
+
416
+ export function hydrationInsertionPoint(): readonly [parent: Node, before: Node] | undefined {
417
+ return activeInsertionPoint
418
+ }
419
+
420
+ export function hydrationCursor(parent: Node): Node | null | undefined {
421
+ const state = activeHydration
422
+ return state === undefined ? undefined : cursor(state, parent)
423
+ }
424
+
425
+ export function hydrationRangeParent(start: Comment, end: Comment): Node | undefined {
426
+ if (activeHydration === undefined) return undefined
427
+ const parent = start.parentNode
428
+ if (parent === null || end.parentNode !== parent) throw mismatch('detached server marker range')
429
+ return parent
430
+ }
431
+
432
+ function requireHydration(): HydrationState {
433
+ if (activeHydration === undefined) throw new Error('no Vidact hydration is active')
434
+ return activeHydration
435
+ }
436
+
437
+ function cursor(state: HydrationState, parent: Node): Node | null {
438
+ if (!state.cursors.has(parent)) state.cursors.set(parent, initialCursor(parent))
439
+ return state.cursors.get(parent) ?? null
440
+ }
441
+
442
+ function initialCursor(parent: Node): Node | null {
443
+ return parent.firstChild
444
+ }
445
+
446
+ function enterHydrationSlot(state: HydrationState, parent: Node): void {
447
+ const slots = state.slots.get(parent) ?? []
448
+ let start = cursor(state, parent)
449
+ while (isMarker(start, `${HYDRATION_PREFIX}:b`)) {
450
+ slots.push(findClosingSibling(start, `${HYDRATION_PREFIX}:b`))
451
+ start = start.nextSibling
452
+ }
453
+ if (slots.length !== 0) state.slots.set(parent, slots)
454
+ state.cursors.set(parent, start)
455
+ }
456
+
457
+ function setHydrationCursor(state: HydrationState, parent: Node, value: Node | null): void {
458
+ const slots = state.slots.get(parent)
459
+ let next = value
460
+ if (slots !== undefined) {
461
+ for (;;) {
462
+ const end = slots.at(-1)
463
+ if (end !== next) break
464
+ slots.pop()
465
+ next = end.nextSibling
466
+ }
467
+ }
468
+ state.cursors.set(parent, next)
469
+ }
470
+
471
+ function mismatch(message: string): HydrationMismatch {
472
+ return new HydrationMismatch(message)
473
+ }
474
+
475
+ /** `DIV.class-a[data-x]` — enough of an element to find it in the server markup. */
476
+ function describeNode(node: Node | null | undefined): string {
477
+ if (!(node instanceof Element)) return node?.nodeName ?? 'null'
478
+ const classes = node.classList.length === 0 ? '' : `.${[...node.classList].join('.')}`
479
+ const data = [...node.attributes]
480
+ .filter((attribute) => attribute.name.startsWith('data-') || attribute.name === 'id')
481
+ .map((attribute) => `[${attribute.name}]`)
482
+ .join('')
483
+ return `${node.nodeName}${classes}${data}`
484
+ }
485
+
486
+ function markerValue(node: Node): string | undefined {
487
+ return node instanceof Comment ? node.data : undefined
488
+ }
489
+
490
+ function isMarker(node: Node | null, value: string): node is Comment {
491
+ return markerValue(node as Node) === value
492
+ }
493
+
494
+ function findClosingSibling(start: Comment, kind: string): Comment {
495
+ let depth = 0
496
+ for (let node = start.nextSibling; node !== null; node = node.nextSibling) {
497
+ if (isMarker(node, kind)) {
498
+ depth += 1
499
+ continue
500
+ }
501
+ if (!isMarker(node, `/${kind}`)) continue
502
+ if (depth === 0) return node
503
+ depth -= 1
504
+ }
505
+ throw mismatch(`missing closing ${kind} marker`)
506
+ }
507
+
508
+ /** Paired markers; `p` (pending fallback) is a lone marker and stays out of the stacks. */
509
+ const RANGE_MARKER = new RegExp(`^\\/?${HYDRATION_PREFIX}:([abchr])$`)
510
+
511
+ function scanRanges(host: ParentNode): {
512
+ roots: HydrationRange[]
513
+ components: HydrationRange[]
514
+ } {
515
+ const roots: HydrationRange[] = []
516
+ const components: HydrationRange[] = []
517
+ const stacks = new Map<string, Comment[]>()
518
+ const walker = document.createTreeWalker(host, NodeFilter.SHOW_COMMENT)
519
+ for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
520
+ const comment = node as Comment
521
+ const match = RANGE_MARKER.exec(comment.data)
522
+ if (match === null) continue
523
+ const kind = match[1] as 'a' | 'b' | 'c' | 'h' | 'r'
524
+ if (!comment.data.startsWith('/')) {
525
+ const stack = stacks.get(kind) ?? []
526
+ stack.push(comment)
527
+ stacks.set(kind, stack)
528
+ continue
529
+ }
530
+ const start = stacks.get(kind)?.pop()
531
+ if (start === undefined) throw mismatch(`orphan closing ${HYDRATION_PREFIX}:${kind} marker`)
532
+ if (kind === 'r') roots.push([start, comment])
533
+ if (kind === 'c') components.push([start, comment])
534
+ }
535
+ if ([...stacks.values()].some((stack) => stack.length !== 0)) {
536
+ throw mismatch(`unclosed ${HYDRATION_PREFIX} hydration marker`)
537
+ }
538
+ components.sort(([left], [right]) => {
539
+ const position = left.compareDocumentPosition(right)
540
+ if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1
541
+ if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1
542
+ return 0
543
+ })
544
+ return { roots, components }
545
+ }
546
+
547
+ function collectPostorderElements(root: HydrationRange): Element[] {
548
+ const elements: Element[] = []
549
+ const visit = (element: Element): void => {
550
+ visitChildren(element)
551
+ elements.push(element)
552
+ }
553
+ const visitChildren = (parent: Node): void => {
554
+ let opaqueDepth = 0
555
+ for (const child of parent.childNodes) {
556
+ if (isMarker(child, `${HYDRATION_PREFIX}:h`)) {
557
+ opaqueDepth += 1
558
+ continue
559
+ }
560
+ if (isMarker(child, `/${HYDRATION_PREFIX}:h`)) {
561
+ opaqueDepth -= 1
562
+ continue
563
+ }
564
+ if (opaqueDepth === 0 && child instanceof Element) visit(child)
565
+ }
566
+ if (opaqueDepth !== 0) throw mismatch(`unbalanced ${HYDRATION_PREFIX} raw HTML marker range`)
567
+ }
568
+ let opaqueDepth = 0
569
+ for (let node = root[0].nextSibling; node !== null && node !== root[1]; node = node.nextSibling) {
570
+ if (isMarker(node, `${HYDRATION_PREFIX}:h`)) {
571
+ opaqueDepth += 1
572
+ continue
573
+ }
574
+ if (isMarker(node, `/${HYDRATION_PREFIX}:h`)) {
575
+ opaqueDepth -= 1
576
+ continue
577
+ }
578
+ if (opaqueDepth === 0 && node instanceof Element) visit(node)
579
+ }
580
+ if (opaqueDepth !== 0) throw mismatch(`unbalanced ${HYDRATION_PREFIX} raw HTML marker range`)
581
+ return elements
582
+ }
583
+
584
+ function collectElementComponents(host: ParentNode): WeakMap<Element, Comment> {
585
+ const owners = new WeakMap<Element, Comment>()
586
+ const components: Comment[] = []
587
+ const walker = document.createTreeWalker(host, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT)
588
+ for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
589
+ if (isMarker(node, `${HYDRATION_PREFIX}:c`)) {
590
+ components.push(node)
591
+ continue
592
+ }
593
+ if (isMarker(node, `/${HYDRATION_PREFIX}:c`)) {
594
+ components.pop()
595
+ continue
596
+ }
597
+ if (node instanceof Element) {
598
+ const owner = components.at(-1)
599
+ if (owner !== undefined) owners.set(node, owner)
600
+ }
601
+ }
602
+ return owners
603
+ }
604
+
605
+ function containsArrayMarker(element: Element): boolean {
606
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_COMMENT)
607
+ for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
608
+ if (isMarker(node, `${HYDRATION_PREFIX}:a`)) return true
609
+ }
610
+ return false
611
+ }
612
+
613
+ export function installHydration(): void {
614
+ installHydrationOperations({
615
+ begin: beginHydration,
616
+ finish: finishHydration,
617
+ rootMarkers: hydrationRootMarkers,
618
+ active: isHydrating,
619
+ createFragment: createHydrationFragment,
620
+ fragmentChildren: hydrationFragmentChildren,
621
+ claimElement: claimHydrationElement,
622
+ noteStructuralParent: noteHydrationStructuralParent,
623
+ claimComponentRange: claimHydrationComponentRange,
624
+ withComponentRange: withHydrationComponentRange,
625
+ withCursor: withHydrationCursor,
626
+ claimNode: claimHydrationNode,
627
+ claimComponentMount: claimHydrationComponentMount,
628
+ claimText: claimHydrationText,
629
+ claimTextRange: claimHydrationTextRange,
630
+ claimArrayRange: claimHydrationArrayRange,
631
+ finishArrayRange: finishHydrationArrayRange,
632
+ claimSlotRange: claimHydrationSlotRange,
633
+ borrowSlotRange: borrowHydrationSlotRange,
634
+ claimSuspenseFallback: claimHydrationSuspenseFallback,
635
+ skipRange: skipHydrationRange,
636
+ withoutHydration,
637
+ withInsertion: withHydrationInsertion,
638
+ insertionPoint: hydrationInsertionPoint,
639
+ cursor: hydrationCursor,
640
+ rangeParent: hydrationRangeParent,
641
+ })
642
+ }