fibrae 0.1.0

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 (70) hide show
  1. package/dist/components.d.ts +40 -0
  2. package/dist/components.js +63 -0
  3. package/dist/components.js.map +1 -0
  4. package/dist/core.d.ts +25 -0
  5. package/dist/core.js +46 -0
  6. package/dist/core.js.map +1 -0
  7. package/dist/dom.d.ts +16 -0
  8. package/dist/dom.js +67 -0
  9. package/dist/dom.js.map +1 -0
  10. package/dist/fiber-render.d.ts +33 -0
  11. package/dist/fiber-render.js +1069 -0
  12. package/dist/fiber-render.js.map +1 -0
  13. package/dist/h.d.ts +19 -0
  14. package/dist/h.js +26 -0
  15. package/dist/h.js.map +1 -0
  16. package/dist/hydration.d.ts +30 -0
  17. package/dist/hydration.js +375 -0
  18. package/dist/hydration.js.map +1 -0
  19. package/dist/index.d.ts +26 -0
  20. package/dist/index.js +28 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/jsx-runtime/index.d.ts +29 -0
  23. package/dist/jsx-runtime/index.js +61 -0
  24. package/dist/jsx-runtime/index.js.map +1 -0
  25. package/dist/render.d.ts +19 -0
  26. package/dist/render.js +325 -0
  27. package/dist/render.js.map +1 -0
  28. package/dist/router/History.d.ts +129 -0
  29. package/dist/router/History.js +241 -0
  30. package/dist/router/History.js.map +1 -0
  31. package/dist/router/Link.d.ts +52 -0
  32. package/dist/router/Link.js +131 -0
  33. package/dist/router/Link.js.map +1 -0
  34. package/dist/router/Navigator.d.ts +108 -0
  35. package/dist/router/Navigator.js +225 -0
  36. package/dist/router/Navigator.js.map +1 -0
  37. package/dist/router/Route.d.ts +65 -0
  38. package/dist/router/Route.js +143 -0
  39. package/dist/router/Route.js.map +1 -0
  40. package/dist/router/Router.d.ts +167 -0
  41. package/dist/router/Router.js +328 -0
  42. package/dist/router/Router.js.map +1 -0
  43. package/dist/router/RouterBuilder.d.ts +128 -0
  44. package/dist/router/RouterBuilder.js +112 -0
  45. package/dist/router/RouterBuilder.js.map +1 -0
  46. package/dist/router/RouterOutlet.d.ts +57 -0
  47. package/dist/router/RouterOutlet.js +132 -0
  48. package/dist/router/RouterOutlet.js.map +1 -0
  49. package/dist/router/RouterState.d.ts +102 -0
  50. package/dist/router/RouterState.js +94 -0
  51. package/dist/router/RouterState.js.map +1 -0
  52. package/dist/router/index.d.ts +28 -0
  53. package/dist/router/index.js +31 -0
  54. package/dist/router/index.js.map +1 -0
  55. package/dist/runtime.d.ts +55 -0
  56. package/dist/runtime.js +68 -0
  57. package/dist/runtime.js.map +1 -0
  58. package/dist/scope-utils.d.ts +14 -0
  59. package/dist/scope-utils.js +29 -0
  60. package/dist/scope-utils.js.map +1 -0
  61. package/dist/server.d.ts +112 -0
  62. package/dist/server.js +313 -0
  63. package/dist/server.js.map +1 -0
  64. package/dist/shared.d.ts +136 -0
  65. package/dist/shared.js +53 -0
  66. package/dist/shared.js.map +1 -0
  67. package/dist/tracking.d.ts +23 -0
  68. package/dist/tracking.js +53 -0
  69. package/dist/tracking.js.map +1 -0
  70. package/package.json +62 -0
@@ -0,0 +1,1069 @@
1
+ /**
2
+ * Fiber-based rendering implementation.
3
+ *
4
+ * This module implements a proper fiber reconciliation system that:
5
+ * - Uses a fiber tree for incremental rendering
6
+ * - Supports key-based diffing for efficient list updates
7
+ * - Two-phase rendering: render phase builds fiber tree, commit phase touches DOM
8
+ * - Function components have `dom: Option.none()` - no wrapper spans!
9
+ *
10
+ * This fixes SSR hydration by producing identical DOM structure on server and client.
11
+ */
12
+ import * as Effect from "effect/Effect";
13
+ import * as Stream from "effect/Stream";
14
+ import * as Scope from "effect/Scope";
15
+ import * as Ref from "effect/Ref";
16
+ import * as Exit from "effect/Exit";
17
+ import * as Option from "effect/Option";
18
+ import * as Deferred from "effect/Deferred";
19
+ import * as Context from "effect/Context";
20
+ import * as FiberRef from "effect/FiberRef";
21
+ import * as Cause from "effect/Cause";
22
+ import { Atom, Registry as AtomRegistry } from "@effect-atom/atom";
23
+ import { isEvent, isProperty, isStream, } from "./shared.js";
24
+ import { FibraeRuntime, runForkWithRuntime } from "./runtime.js";
25
+ import { setDomProperty, attachEventListeners } from "./dom.js";
26
+ import { normalizeToStream, makeTrackingRegistry } from "./tracking.js";
27
+ import { h } from "./h.js";
28
+ // =============================================================================
29
+ // Stream Subscription Helper
30
+ // =============================================================================
31
+ /**
32
+ * Subscribe to a component's output stream, handling first value and subsequent emissions.
33
+ *
34
+ * Returns a deferred that will be completed with the first emitted value.
35
+ * Subsequent emissions update latestStreamValue and queue re-renders.
36
+ * Errors are forwarded to handleFiberError.
37
+ *
38
+ * @param stream - The component's output stream
39
+ * @param fiberRef - Mutable reference to the current fiber (for re-renders after reconciliation)
40
+ * @param scope - Scope to fork the subscription into
41
+ *
42
+ * @typeParam E - Error type for the deferred (use `never` for die mode, stream error type for fail mode)
43
+ */
44
+ const subscribeComponentStream = (stream, fiberRef, scope) => Effect.gen(function* () {
45
+ const firstValueDeferred = yield* Deferred.make();
46
+ const subscription = Stream.runForEach(stream, (vElement) => Effect.gen(function* () {
47
+ const done = yield* Deferred.isDone(firstValueDeferred);
48
+ const currentFiber = fiberRef.current;
49
+ if (!done) {
50
+ yield* Deferred.succeed(firstValueDeferred, vElement);
51
+ }
52
+ else {
53
+ // Subsequent emissions - queue re-render
54
+ currentFiber.latestStreamValue = Option.some(vElement);
55
+ yield* queueFiberForRerender(currentFiber);
56
+ }
57
+ })).pipe(Effect.catchAllCause((cause) => Effect.gen(function* () {
58
+ const done = yield* Deferred.isDone(firstValueDeferred);
59
+ if (!done) {
60
+ yield* Deferred.failCause(firstValueDeferred, cause);
61
+ }
62
+ const currentFiber = fiberRef.current;
63
+ yield* handleFiberError(currentFiber, cause);
64
+ })));
65
+ yield* Effect.forkIn(subscription, scope);
66
+ return firstValueDeferred;
67
+ });
68
+ // =============================================================================
69
+ // Fiber Creation Helpers
70
+ // =============================================================================
71
+ const createFiber = (type, props, parent, alternate, effectTag) => ({
72
+ type,
73
+ props,
74
+ dom: Option.none(),
75
+ parent,
76
+ child: Option.none(),
77
+ sibling: Option.none(),
78
+ alternate,
79
+ effectTag,
80
+ componentScope: Option.none(),
81
+ accessedAtoms: Option.none(),
82
+ latestStreamValue: Option.none(),
83
+ childFirstCommitDeferred: Option.none(),
84
+ fiberRef: Option.none(),
85
+ isMultiEmissionStream: false,
86
+ errorBoundary: Option.none(),
87
+ suspense: Option.none(),
88
+ renderContext: Option.none(),
89
+ isParked: false,
90
+ isUnparking: false,
91
+ });
92
+ /**
93
+ * Check if a fiber's type matches a specific element type string.
94
+ */
95
+ const fiberTypeIs = (fiber, expected) => fiber.type.pipe(Option.map((t) => t === expected), Option.getOrElse(() => false));
96
+ /**
97
+ * Check if a fiber's type is a function (i.e., a function component).
98
+ */
99
+ const fiberTypeIsFunction = (fiber) => fiber.type.pipe(Option.map((t) => typeof t === "function"), Option.getOrElse(() => false));
100
+ /**
101
+ * Check if a fiber is a virtual element (no DOM node created).
102
+ * Returns true for root fiber (no type) or FRAGMENT type.
103
+ */
104
+ const fiberIsVirtualElement = (fiber) => fiber.type.pipe(Option.map((t) => t === "FRAGMENT"), Option.getOrElse(() => true) // Root fiber has no type
105
+ );
106
+ /**
107
+ * Get a fiber's required componentScope or die with a message.
108
+ */
109
+ const getComponentScopeOrDie = (fiber, msg) => Option.match(fiber.componentScope, {
110
+ onNone: () => Effect.die(msg),
111
+ onSome: (s) => Effect.succeed(s),
112
+ });
113
+ // =============================================================================
114
+ // Queue Fiber for Re-render (batched updates)
115
+ // =============================================================================
116
+ const queueFiberForRerender = (fiber) => Effect.gen(function* () {
117
+ const runtime = yield* FibraeRuntime;
118
+ const stateRef = runtime.fiberState;
119
+ const didSchedule = yield* Ref.modify(stateRef, (s) => {
120
+ const alreadyQueued = s.renderQueue.has(fiber);
121
+ const newQueue = alreadyQueued ? s.renderQueue : new Set([...s.renderQueue, fiber]);
122
+ const shouldScheduleNow = !s.batchScheduled;
123
+ const next = {
124
+ ...s,
125
+ renderQueue: newQueue,
126
+ batchScheduled: s.batchScheduled || shouldScheduleNow,
127
+ };
128
+ return [shouldScheduleNow, next];
129
+ });
130
+ if (didSchedule) {
131
+ queueMicrotask(() => {
132
+ runForkWithRuntime(runtime)(processBatch());
133
+ });
134
+ }
135
+ });
136
+ // =============================================================================
137
+ // Process Batch (handles queued re-renders)
138
+ // =============================================================================
139
+ const processBatch = () => Effect.gen(function* () {
140
+ const runtime = yield* FibraeRuntime;
141
+ const stateRef = runtime.fiberState;
142
+ const stateSnapshot = yield* Ref.get(stateRef);
143
+ const batch = Array.from(stateSnapshot.renderQueue);
144
+ // Clear the queue but keep batchScheduled = true to prevent concurrent batches
145
+ yield* Ref.update(stateRef, (s) => ({
146
+ ...s,
147
+ renderQueue: new Set(),
148
+ // DO NOT set batchScheduled = false here - keep it true during workLoop
149
+ }));
150
+ if (batch.length === 0) {
151
+ // No work to do - reset batchScheduled
152
+ yield* Ref.update(stateRef, (s) => ({ ...s, batchScheduled: false }));
153
+ return;
154
+ }
155
+ yield* Option.match(stateSnapshot.currentRoot, {
156
+ onNone: () => Effect.void,
157
+ onSome: (currentRoot) => Effect.gen(function* () {
158
+ yield* Ref.update(stateRef, (s) => ({
159
+ ...s,
160
+ wipRoot: Option.some(createFiber(currentRoot.type, currentRoot.props, Option.none(), Option.some(currentRoot), Option.none())),
161
+ deletions: [],
162
+ }));
163
+ // Copy dom from currentRoot to wipRoot
164
+ const newState = yield* Ref.get(stateRef);
165
+ Option.match(newState.wipRoot, {
166
+ onNone: () => { },
167
+ onSome: (wip) => {
168
+ wip.dom = currentRoot.dom;
169
+ },
170
+ });
171
+ yield* Ref.update(stateRef, (s) => ({
172
+ ...s,
173
+ nextUnitOfWork: s.wipRoot,
174
+ }));
175
+ yield* workLoop(runtime);
176
+ }),
177
+ });
178
+ // After workLoop completes, check if more work was queued during processing
179
+ const afterState = yield* Ref.get(stateRef);
180
+ if (afterState.renderQueue.size > 0) {
181
+ // More work was queued during the batch - process it immediately
182
+ yield* processBatch();
183
+ }
184
+ else {
185
+ // No more work - allow new batches to be scheduled
186
+ yield* Ref.update(stateRef, (s) => ({ ...s, batchScheduled: false }));
187
+ }
188
+ });
189
+ // =============================================================================
190
+ // Fiber Tree Walking Helpers
191
+ // =============================================================================
192
+ /**
193
+ * Walk up the fiber tree from the starting fiber, returning the first ancestor
194
+ * (including the starting fiber) that matches the predicate.
195
+ */
196
+ const findAncestor = (fiber, predicate) => {
197
+ let current = Option.some(fiber);
198
+ while (Option.isSome(current)) {
199
+ if (predicate(current.value))
200
+ return current;
201
+ current = current.value.parent;
202
+ }
203
+ return Option.none();
204
+ };
205
+ /**
206
+ * Walk up the fiber tree from the starting fiber's parent, returning the first
207
+ * ancestor that matches the predicate (excludes the starting fiber itself).
208
+ */
209
+ const findAncestorExcludingSelf = (fiber, predicate) => {
210
+ let current = fiber.parent;
211
+ while (Option.isSome(current)) {
212
+ if (predicate(current.value))
213
+ return current;
214
+ current = current.value.parent;
215
+ }
216
+ return Option.none();
217
+ };
218
+ /**
219
+ * Link an array of fibers as siblings under a parent fiber.
220
+ * Sets parent.child to the first fiber and chains the rest via sibling pointers.
221
+ */
222
+ const linkFibersAsSiblings = (fibers, parent) => {
223
+ if (fibers.length === 0) {
224
+ parent.child = Option.none();
225
+ return;
226
+ }
227
+ parent.child = Option.some(fibers[0]);
228
+ for (let i = 1; i < fibers.length; i++) {
229
+ fibers[i - 1].sibling = Option.some(fibers[i]);
230
+ }
231
+ };
232
+ /**
233
+ * Find the nearest ancestor with a DOM node (walks up from parent).
234
+ */
235
+ const findDomParent = (fiber) => {
236
+ const ancestor = findAncestorExcludingSelf(fiber, (f) => Option.isSome(f.dom));
237
+ return Option.flatMap(ancestor, (f) => f.dom);
238
+ };
239
+ // =============================================================================
240
+ // Error Boundary Support
241
+ // =============================================================================
242
+ const findNearestErrorBoundary = (fiber) => findAncestor(fiber, (f) => Option.isSome(f.errorBoundary));
243
+ const handleFiberError = (fiber, cause) => Effect.gen(function* () {
244
+ const runtime = yield* FibraeRuntime;
245
+ const stateRef = runtime.fiberState;
246
+ const state = yield* Ref.get(stateRef);
247
+ const boundaryOpt = findNearestErrorBoundary(fiber);
248
+ if (Option.isSome(boundaryOpt)) {
249
+ const boundary = boundaryOpt.value;
250
+ const cfg = Option.getOrElse(boundary.errorBoundary, () => ({
251
+ fallback: h("div", {}, []),
252
+ hasError: false,
253
+ onError: undefined,
254
+ }));
255
+ cfg.onError?.(cause);
256
+ cfg.hasError = true;
257
+ boundary.errorBoundary = Option.some(cfg);
258
+ // Check if we're in initial render (no currentRoot yet)
259
+ if (Option.isNone(state.currentRoot)) {
260
+ // During initial render: re-reconcile boundary with fallback immediately
261
+ // This replaces the errored child with the fallback element
262
+ yield* reconcileChildren(boundary, [cfg.fallback]);
263
+ // Return the boundary's new child (fallback) so work continues
264
+ return boundary.child;
265
+ }
266
+ else {
267
+ // After initial render: queue for re-render
268
+ yield* queueFiberForRerender(boundary);
269
+ return Option.none();
270
+ }
271
+ }
272
+ else {
273
+ yield* Effect.logError("Unhandled error without ErrorBoundary", cause);
274
+ return Option.none();
275
+ }
276
+ });
277
+ // =============================================================================
278
+ // Suspense Support
279
+ // =============================================================================
280
+ /**
281
+ * Find the nearest Suspense boundary by walking up the fiber tree.
282
+ */
283
+ const findNearestSuspenseBoundary = (fiber) => findAncestorExcludingSelf(fiber, (f) => Option.isSome(f.suspense));
284
+ /**
285
+ * Get the threshold from the nearest Suspense boundary.
286
+ * Returns 0 if no boundary (wait indefinitely).
287
+ */
288
+ const getSuspenseThreshold = (fiber) => {
289
+ const boundary = findNearestSuspenseBoundary(fiber);
290
+ return boundary.pipe(Option.flatMap((b) => b.suspense), Option.map((cfg) => cfg.threshold), Option.getOrElse(() => 0));
291
+ };
292
+ /**
293
+ * Called when a stream component's threshold expires before first emission.
294
+ * Parks the fiber and switches the boundary to show fallback.
295
+ */
296
+ const handleFiberSuspension = (fiber) => Effect.gen(function* () {
297
+ const boundaryOpt = findNearestSuspenseBoundary(fiber);
298
+ if (Option.isNone(boundaryOpt)) {
299
+ // No Suspense boundary - just continue waiting
300
+ return;
301
+ }
302
+ const boundary = boundaryOpt.value;
303
+ const config = Option.getOrThrow(boundary.suspense);
304
+ if (config.showingFallback) {
305
+ // Already suspended - first suspension wins
306
+ return;
307
+ }
308
+ // Create deferred for parked fiber completion
309
+ const parkedComplete = yield* Deferred.make();
310
+ // Mark fiber as parked - its scope should not be closed on deletion
311
+ fiber.isParked = true;
312
+ // Park the fiber and switch to fallback
313
+ config.showingFallback = true;
314
+ config.parkedFiber = Option.some(fiber);
315
+ config.parkedComplete = Option.some(parkedComplete);
316
+ // Trigger re-render of boundary with fallback
317
+ yield* queueFiberForRerender(boundary);
318
+ });
319
+ /**
320
+ * Called when a parked fiber finally gets its first emission.
321
+ * Signals the boundary to swap back to showing children.
322
+ */
323
+ const signalFiberReady = (fiber) => Effect.gen(function* () {
324
+ const boundaryOpt = findNearestSuspenseBoundary(fiber);
325
+ if (Option.isNone(boundaryOpt))
326
+ return;
327
+ const boundary = boundaryOpt.value;
328
+ const config = Option.getOrThrow(boundary.suspense);
329
+ // Unpark the fiber - it's ready now, scope can be closed normally on next deletion
330
+ fiber.isParked = false;
331
+ // Signal that parked fiber is ready
332
+ yield* config.parkedComplete.pipe(Option.map((deferred) => Deferred.succeed(deferred, undefined)), Option.getOrElse(() => Effect.void));
333
+ // Trigger re-render to swap fallback → children
334
+ yield* queueFiberForRerender(boundary);
335
+ });
336
+ // =============================================================================
337
+ // Update Function Component
338
+ // =============================================================================
339
+ const updateFunctionComponent = (fiber, runtime) => Effect.gen(function* () {
340
+ // Initialize deferred for child first commit signaling
341
+ if (Option.isNone(fiber.childFirstCommitDeferred)) {
342
+ fiber.childFirstCommitDeferred = Option.some(yield* Deferred.make());
343
+ }
344
+ // Capture current context during render phase for event handlers in commit phase
345
+ // This includes services like Navigator, RouterHandlers, etc.
346
+ const currentContext = (yield* FiberRef.get(FiberRef.currentContext));
347
+ fiber.renderContext = Option.some(currentContext);
348
+ // Check if we can reuse cached stream value from alternate
349
+ const hasAlternate = Option.isSome(fiber.alternate);
350
+ const hasCachedValue = fiber.alternate.pipe(Option.map((alt) => Option.isSome(alt.latestStreamValue) && alt.isMultiEmissionStream), Option.getOrElse(() => false));
351
+ if (hasAlternate && hasCachedValue) {
352
+ // Reuse cached value from alternate (stream component that emitted multiple values)
353
+ const alt = Option.getOrThrow(fiber.alternate);
354
+ const vElement = Option.getOrThrow(alt.latestStreamValue);
355
+ fiber.latestStreamValue = alt.latestStreamValue;
356
+ fiber.accessedAtoms = alt.accessedAtoms;
357
+ fiber.componentScope = alt.componentScope;
358
+ alt.componentScope = Option.none(); // Transfer ownership
359
+ fiber.fiberRef = alt.fiberRef;
360
+ Option.match(fiber.fiberRef, {
361
+ onNone: () => { },
362
+ onSome: (ref) => {
363
+ ref.current = fiber;
364
+ },
365
+ });
366
+ fiber.isMultiEmissionStream = alt.isMultiEmissionStream;
367
+ yield* reconcileChildren(fiber, [vElement]);
368
+ return;
369
+ }
370
+ // Check if this fiber is being restored from parked (suspended) state
371
+ // If so, skip re-execution and use the cached latestStreamValue
372
+ if (fiber.isUnparking && Option.isSome(fiber.latestStreamValue)) {
373
+ fiber.isUnparking = false; // Clear flag
374
+ const vElement = Option.getOrThrow(fiber.latestStreamValue);
375
+ yield* reconcileChildren(fiber, [vElement]);
376
+ return;
377
+ }
378
+ // Set up atom tracking
379
+ const accessedAtoms = new Set();
380
+ const trackingRegistry = makeTrackingRegistry(runtime.registry, accessedAtoms);
381
+ const contextWithTracking = Context.add(currentContext, AtomRegistry.AtomRegistry, trackingRegistry);
382
+ // Invoke the component
383
+ const output = yield* Option.match(fiber.type, {
384
+ onNone: () => Effect.die("updateFunctionComponent called with no type"),
385
+ onSome: (type) => {
386
+ if (typeof type !== "function") {
387
+ return Effect.die("updateFunctionComponent called with non-function type");
388
+ }
389
+ const component = type;
390
+ return Effect.sync(() => component(fiber.props));
391
+ },
392
+ });
393
+ // Fast path: if component returns a plain VElement (not Effect/Stream),
394
+ // and it's a special element type, just reconcile directly without stream machinery.
395
+ // This handles wrapper components like Suspense and ErrorBoundary efficiently.
396
+ if (!Effect.isEffect(output) && !isStream(output)) {
397
+ const vElement = output;
398
+ if (typeof vElement === "object" && vElement !== null && "type" in vElement) {
399
+ const elementType = vElement.type;
400
+ if (elementType === "SUSPENSE" || elementType === "ERROR_BOUNDARY" || elementType === "FRAGMENT") {
401
+ // Simple wrapper component - just reconcile with the VElement directly
402
+ yield* reconcileChildren(fiber, [vElement]);
403
+ return;
404
+ }
405
+ }
406
+ }
407
+ // Check if it's a multi-emission stream
408
+ fiber.isMultiEmissionStream = isStream(output);
409
+ // Normalize to stream and provide context
410
+ const stream = normalizeToStream(output).pipe(Stream.provideContext(contextWithTracking));
411
+ // Create scope for this component
412
+ yield* resubscribeFiber(fiber);
413
+ fiber.accessedAtoms = Option.some(accessedAtoms);
414
+ const scope = yield* getComponentScopeOrDie(fiber, "Expected componentScope to be created by resubscribeFiber");
415
+ // Set up fiber ref for stream subscriptions
416
+ const fiberRef = fiber.fiberRef.pipe(Option.getOrElse(() => ({ current: fiber })));
417
+ fiber.fiberRef = Option.some(fiberRef);
418
+ // Subscribe to component stream - errors become defects via "die" mode
419
+ const firstValueDeferred = yield* subscribeComponentStream(stream, fiberRef, scope);
420
+ // Get threshold from nearest Suspense boundary
421
+ const threshold = getSuspenseThreshold(fiber);
422
+ if (threshold > 0) {
423
+ // Race first value vs threshold
424
+ const result = yield* Effect.race(Deferred.await(firstValueDeferred).pipe(Effect.map((v) => ({ _tag: "value", value: v }))), Effect.sleep(`${threshold} millis`).pipe(Effect.map(() => ({ _tag: "timeout" }))));
425
+ if (result._tag === "timeout") {
426
+ // Threshold expired - signal suspension to boundary
427
+ yield* handleFiberSuspension(fiber);
428
+ // Fork background work: wait for value, then signal ready
429
+ // This allows the render loop to continue and show fallback
430
+ yield* Effect.forkIn(Effect.gen(function* () {
431
+ const value = yield* Deferred.await(firstValueDeferred);
432
+ const currentFiber = fiberRef.current;
433
+ currentFiber.latestStreamValue = Option.some(value);
434
+ yield* signalFiberReady(currentFiber);
435
+ }), scope);
436
+ // Return early - don't reconcile children yet
437
+ // The Suspense boundary will show fallback via queued re-render
438
+ return;
439
+ }
440
+ else {
441
+ // Value arrived before threshold - no suspension
442
+ fiber.latestStreamValue = Option.some(result.value);
443
+ yield* reconcileChildren(fiber, [result.value]);
444
+ }
445
+ }
446
+ else {
447
+ // No threshold (0) - wait indefinitely, no suspension possible
448
+ const firstVElement = yield* Deferred.await(firstValueDeferred);
449
+ fiber.latestStreamValue = Option.some(firstVElement);
450
+ yield* reconcileChildren(fiber, [firstVElement]);
451
+ }
452
+ // Subscribe to atom changes
453
+ yield* subscribeFiberAtoms(fiber, accessedAtoms, runtime);
454
+ });
455
+ // =============================================================================
456
+ // Perform Unit of Work
457
+ // =============================================================================
458
+ const performUnitOfWork = (fiber, runtime) => Effect.gen(function* () {
459
+ const isFunctionComponent = fiberTypeIsFunction(fiber);
460
+ const eff = isFunctionComponent
461
+ ? updateFunctionComponent(fiber, runtime)
462
+ : updateHostComponent(fiber, runtime);
463
+ const exited = yield* Effect.exit(eff);
464
+ if (Exit.isFailure(exited)) {
465
+ // handleFiberError returns the next fiber to process (if recovery happened)
466
+ return yield* handleFiberError(fiber, exited.cause);
467
+ }
468
+ // Return next unit of work: child, then sibling, then uncle
469
+ if (Option.isSome(fiber.child)) {
470
+ return fiber.child;
471
+ }
472
+ let currentFiber = Option.some(fiber);
473
+ while (Option.isSome(currentFiber)) {
474
+ if (Option.isSome(currentFiber.value.sibling)) {
475
+ return currentFiber.value.sibling;
476
+ }
477
+ currentFiber = currentFiber.value.parent;
478
+ }
479
+ return Option.none();
480
+ });
481
+ // =============================================================================
482
+ // Atom Subscription
483
+ // =============================================================================
484
+ const resubscribeFiber = (fiber) => Effect.gen(function* () {
485
+ // Close old scope if exists
486
+ yield* Option.match(fiber.componentScope, {
487
+ onNone: () => Effect.void,
488
+ onSome: (scope) => Scope.close(scope, Exit.void),
489
+ });
490
+ // Create new scope
491
+ const newScope = yield* Scope.make();
492
+ fiber.componentScope = Option.some(newScope);
493
+ });
494
+ const subscribeFiberAtoms = (fiber, accessedAtoms, runtime) => Effect.gen(function* () {
495
+ const scope = yield* getComponentScopeOrDie(fiber, "subscribeFiberAtoms requires an existing componentScope");
496
+ yield* Effect.forEach(accessedAtoms, (atom) => {
497
+ // Drop first emission (current value), subscribe to changes
498
+ const atomStream = AtomRegistry.toStream(runtime.registry, atom).pipe(Stream.drop(1));
499
+ const subscription = Stream.runForEach(atomStream, () => queueFiberForRerender(fiber));
500
+ return Effect.forkIn(subscription, scope);
501
+ }, { discard: true, concurrency: "unbounded" });
502
+ });
503
+ // =============================================================================
504
+ // Update Host Component
505
+ // =============================================================================
506
+ const updateHostComponent = (fiber, runtime) => Effect.gen(function* () {
507
+ // Inherit renderContext from parent fiber (function components capture it during render)
508
+ // This propagates Navigator, RouterHandlers, etc. down to host elements for event handlers
509
+ if (Option.isNone(fiber.renderContext) && Option.isSome(fiber.parent)) {
510
+ fiber.renderContext = fiber.parent.value.renderContext;
511
+ }
512
+ // Handle ERROR_BOUNDARY specially
513
+ const isErrorBoundary = fiberTypeIs(fiber, "ERROR_BOUNDARY");
514
+ if (isErrorBoundary) {
515
+ // Initialize errorBoundary config if not already set
516
+ if (Option.isNone(fiber.errorBoundary)) {
517
+ const fallback = fiber.props.fallback;
518
+ const onError = fiber.props.onError;
519
+ fiber.errorBoundary = Option.some({
520
+ fallback,
521
+ onError,
522
+ hasError: false,
523
+ });
524
+ }
525
+ const config = Option.getOrThrow(fiber.errorBoundary);
526
+ if (config.hasError) {
527
+ // Error state - render fallback instead of children
528
+ yield* reconcileChildren(fiber, [config.fallback]);
529
+ }
530
+ else {
531
+ // Normal state - render children
532
+ const children = fiber.props.children;
533
+ yield* reconcileChildren(fiber, children || []);
534
+ }
535
+ return;
536
+ }
537
+ // Handle SUSPENSE specially
538
+ const isSuspense = fiberTypeIs(fiber, "SUSPENSE");
539
+ if (isSuspense) {
540
+ const fallback = fiber.props.fallback;
541
+ const threshold = fiber.props.threshold ?? 100;
542
+ const children = fiber.props.children;
543
+ // Initialize suspense config if not already set
544
+ if (Option.isNone(fiber.suspense)) {
545
+ fiber.suspense = Option.some({
546
+ fallback,
547
+ threshold,
548
+ showingFallback: false,
549
+ parkedFiber: Option.none(),
550
+ parkedComplete: Option.none(),
551
+ });
552
+ }
553
+ const config = Option.getOrThrow(fiber.suspense);
554
+ // Check if parked fiber has completed (signaled via signalFiberReady)
555
+ const parkedDone = yield* config.parkedComplete.pipe(Option.map((d) => Deferred.isDone(d)), Option.getOrElse(() => Effect.succeed(false)));
556
+ if (parkedDone && config.showingFallback) {
557
+ // Parked fiber is ready - switch back to showing its content
558
+ config.showingFallback = false;
559
+ // Mark the fallback child from alternate (previous render) for deletion
560
+ // The fallback fiber is in the alternate's child, not the current fiber's child
561
+ yield* Option.match(fiber.alternate, {
562
+ onNone: () => Effect.void,
563
+ onSome: (alt) => Option.match(alt.child, {
564
+ onNone: () => Effect.void,
565
+ onSome: (fallbackChild) => Effect.gen(function* () {
566
+ fallbackChild.effectTag = Option.some("DELETION");
567
+ yield* Ref.update(runtime.fiberState, (s) => ({
568
+ ...s,
569
+ deletions: [...s.deletions, fallbackChild],
570
+ }));
571
+ }),
572
+ }),
573
+ });
574
+ // Reuse the parked fiber directly - it already has latestStreamValue
575
+ const parkedFiber = Option.getOrThrow(config.parkedFiber);
576
+ // Re-parent the parked fiber under this suspense boundary
577
+ parkedFiber.parent = Option.some(fiber);
578
+ parkedFiber.sibling = Option.none();
579
+ parkedFiber.effectTag = Option.some("PLACEMENT");
580
+ // Mark as unparking - this tells updateFunctionComponent to skip re-execution
581
+ parkedFiber.isUnparking = true;
582
+ // Set as child of this boundary
583
+ fiber.child = Option.some(parkedFiber);
584
+ // Clear parked state
585
+ config.parkedFiber = Option.none();
586
+ config.parkedComplete = Option.none();
587
+ // Don't reconcile here - let performUnitOfWork handle it
588
+ // The work loop will process parkedFiber next, and updateFunctionComponent
589
+ // will see isUnparking=true and use the cached latestStreamValue
590
+ return;
591
+ }
592
+ if (config.showingFallback) {
593
+ // Show fallback while children are suspended
594
+ yield* reconcileChildren(fiber, [fallback]);
595
+ }
596
+ else {
597
+ // Show children normally (they'll race against threshold in updateFunctionComponent)
598
+ yield* reconcileChildren(fiber, children || []);
599
+ }
600
+ return;
601
+ }
602
+ // Virtual element types don't create DOM - just reconcile children
603
+ const isVirtualElement = fiberIsVirtualElement(fiber);
604
+ if (!isVirtualElement && Option.isNone(fiber.dom)) {
605
+ fiber.dom = Option.some(yield* createDom(fiber, runtime));
606
+ }
607
+ const children = fiber.props.children;
608
+ yield* reconcileChildren(fiber, children || []);
609
+ });
610
+ const createDom = (fiber, runtime) => Effect.gen(function* () {
611
+ const dom = yield* Option.match(fiber.type, {
612
+ onNone: () => Effect.die("createDom called with no type"),
613
+ onSome: (type) => {
614
+ if (typeof type !== "string") {
615
+ return Effect.die("createDom called on function component");
616
+ }
617
+ const node = type === "TEXT_ELEMENT"
618
+ ? document.createTextNode("")
619
+ : document.createElement(type);
620
+ return Effect.succeed(node);
621
+ },
622
+ });
623
+ yield* updateDom(dom, {}, fiber.props, fiber, runtime);
624
+ // Handle ref
625
+ Option.match(Option.fromNullable(fiber.props.ref), {
626
+ onNone: () => { },
627
+ onSome: (ref) => {
628
+ if (typeof ref === "object" && "current" in ref) {
629
+ ref.current = dom;
630
+ }
631
+ },
632
+ });
633
+ return dom;
634
+ });
635
+ // =============================================================================
636
+ // Update DOM
637
+ // =============================================================================
638
+ const isNew = (prev, next) => (key) => prev[key] !== next[key];
639
+ const updateDom = (dom, prevProps, nextProps, ownerFiber, runtime) => Effect.gen(function* () {
640
+ const stateRef = runtime.fiberState;
641
+ const element = dom;
642
+ if (element instanceof Text) {
643
+ if (nextProps.nodeValue !== prevProps.nodeValue) {
644
+ const value = nextProps.nodeValue;
645
+ element.nodeValue = typeof value === "string" || typeof value === "number"
646
+ ? String(value)
647
+ : "";
648
+ }
649
+ return;
650
+ }
651
+ const stateSnapshot = yield* Ref.get(stateRef);
652
+ const el = element;
653
+ const stored = stateSnapshot.listenerStore.get(el) ?? {};
654
+ // Remove old event listeners
655
+ const eventsToRemove = Object.keys(prevProps)
656
+ .filter(isEvent)
657
+ .filter((key) => !(key in nextProps) || isNew(prevProps, nextProps)(key));
658
+ for (const name of eventsToRemove) {
659
+ const eventType = name.toLowerCase().substring(2);
660
+ const wrapper = stored[eventType];
661
+ if (wrapper) {
662
+ el.removeEventListener(eventType, wrapper);
663
+ delete stored[eventType];
664
+ }
665
+ }
666
+ // Update changed properties
667
+ Object.keys(nextProps)
668
+ .filter(isProperty)
669
+ .filter(isNew(prevProps, nextProps))
670
+ .forEach((name) => {
671
+ if (el instanceof HTMLElement) {
672
+ setDomProperty(el, name, nextProps[name]);
673
+ }
674
+ });
675
+ // Add new event listeners
676
+ Object.keys(nextProps)
677
+ .filter(isEvent)
678
+ .filter(isNew(prevProps, nextProps))
679
+ .forEach((name) => {
680
+ const eventType = name.toLowerCase().substring(2);
681
+ const handler = nextProps[name];
682
+ const wrapper = (event) => {
683
+ const result = handler(event);
684
+ if (Effect.isEffect(result)) {
685
+ // Use runForkWithRuntime to get the full application context
686
+ // This provides Navigator, FibraeRuntime, AtomRegistry, etc.
687
+ const effectWithErrorHandling = result.pipe(Effect.catchAllCause((cause) => ownerFiber ? handleFiberError(ownerFiber, cause) : Effect.void));
688
+ runForkWithRuntime(runtime)(effectWithErrorHandling);
689
+ }
690
+ };
691
+ const existing = stored[eventType];
692
+ if (existing) {
693
+ el.removeEventListener(eventType, existing);
694
+ }
695
+ el.addEventListener(eventType, wrapper);
696
+ stored[eventType] = wrapper;
697
+ });
698
+ stateSnapshot.listenerStore.set(el, stored);
699
+ });
700
+ // =============================================================================
701
+ // Reconcile Children (key-based diffing)
702
+ // =============================================================================
703
+ const reconcileChildren = (wipFiber, elements) => Effect.gen(function* () {
704
+ const runtime = yield* FibraeRuntime;
705
+ const stateRef = runtime.fiberState;
706
+ // Collect old children from alternate
707
+ const oldChildren = [];
708
+ if (Option.isSome(wipFiber.alternate)) {
709
+ let current = wipFiber.alternate.value.child;
710
+ while (Option.isSome(current)) {
711
+ oldChildren.push(current.value);
712
+ current = current.value.sibling;
713
+ }
714
+ }
715
+ const getKey = (props) => Option.fromNullable(props ? props.key : undefined);
716
+ // Build maps for keyed and unkeyed old fibers
717
+ const oldByKey = new Map();
718
+ const oldUnkeyed = [];
719
+ for (const f of oldChildren) {
720
+ const keyOpt = getKey(f.props);
721
+ Option.match(keyOpt, {
722
+ onNone: () => oldUnkeyed.push(f),
723
+ onSome: (key) => oldByKey.set(key, f),
724
+ });
725
+ }
726
+ const newFibers = [];
727
+ for (const element of elements) {
728
+ let matchedOldOpt = Option.none();
729
+ const keyOpt = getKey(element.props);
730
+ // Try to match by key first
731
+ matchedOldOpt = Option.match(keyOpt, {
732
+ onNone: () => Option.none(),
733
+ onSome: (key) => {
734
+ const maybe = Option.fromNullable(oldByKey.get(key));
735
+ Option.match(maybe, {
736
+ onNone: () => { },
737
+ onSome: () => oldByKey.delete(key),
738
+ });
739
+ return maybe;
740
+ },
741
+ });
742
+ // Fall back to type matching for unkeyed elements
743
+ if (Option.isNone(matchedOldOpt)) {
744
+ const idx = oldUnkeyed.findIndex((f) => Option.match(f.type, {
745
+ onNone: () => false,
746
+ onSome: (fType) => fType === element.type,
747
+ }));
748
+ if (idx >= 0) {
749
+ matchedOldOpt = Option.some(oldUnkeyed[idx]);
750
+ oldUnkeyed.splice(idx, 1);
751
+ }
752
+ }
753
+ // Create new fiber based on match
754
+ const newFiber = yield* Option.match(matchedOldOpt, {
755
+ onNone: () => Effect.succeed(createFiber(Option.some(element.type), element.props, Option.some(wipFiber), Option.none(), Option.some("PLACEMENT"))),
756
+ onSome: (matched) => Effect.gen(function* () {
757
+ const typeMatches = Option.match(matched.type, {
758
+ onNone: () => false,
759
+ onSome: (mType) => mType === element.type,
760
+ });
761
+ if (typeMatches) {
762
+ // UPDATE - reuse DOM, update props
763
+ const fiber = createFiber(matched.type, element.props, Option.some(wipFiber), matchedOldOpt, Option.some("UPDATE"));
764
+ fiber.dom = matched.dom;
765
+ fiber.errorBoundary = matched.errorBoundary;
766
+ fiber.suspense = matched.suspense;
767
+ return fiber;
768
+ }
769
+ else {
770
+ // Type changed - delete old, create new
771
+ matched.effectTag = Option.some("DELETION");
772
+ yield* Ref.update(stateRef, (s) => ({
773
+ ...s,
774
+ deletions: [...s.deletions, matched],
775
+ }));
776
+ return createFiber(Option.some(element.type), element.props, Option.some(wipFiber), Option.none(), Option.some("PLACEMENT"));
777
+ }
778
+ }),
779
+ });
780
+ newFibers.push(newFiber);
781
+ }
782
+ // Mark leftover old fibers for deletion
783
+ const leftovers = [...oldByKey.values(), ...oldUnkeyed];
784
+ yield* Effect.forEach(leftovers, (leftover) => Effect.gen(function* () {
785
+ leftover.effectTag = Option.some("DELETION");
786
+ yield* Ref.update(stateRef, (s) => ({
787
+ ...s,
788
+ deletions: [...s.deletions, leftover],
789
+ }));
790
+ }), { discard: true });
791
+ // Link new fibers as child/sibling chain
792
+ linkFibersAsSiblings(newFibers, wipFiber);
793
+ });
794
+ // =============================================================================
795
+ // Commit Phase
796
+ // =============================================================================
797
+ const deleteFiber = (fiber) => Effect.gen(function* () {
798
+ // Close component scope (unless fiber is parked - its scope must stay alive)
799
+ if (!fiber.isParked && Option.isSome(fiber.componentScope)) {
800
+ yield* Scope.close(fiber.componentScope.value, Exit.void);
801
+ }
802
+ // Recursively delete children
803
+ if (Option.isSome(fiber.child)) {
804
+ yield* deleteFiber(fiber.child.value);
805
+ }
806
+ });
807
+ const commitDeletion = (fiber, domParent) => Option.match(fiber.dom, {
808
+ onSome: (dom) => Effect.sync(() => {
809
+ domParent.removeChild(dom);
810
+ }),
811
+ onNone: () =>
812
+ // Function component - find DOM children
813
+ Effect.iterate(fiber.child, {
814
+ while: (opt) => Option.isSome(opt),
815
+ body: (childOpt) => Effect.gen(function* () {
816
+ const child = childOpt.value;
817
+ yield* commitDeletion(child, domParent);
818
+ return child.sibling;
819
+ }),
820
+ }),
821
+ });
822
+ const commitRoot = (runtime) => Effect.gen(function* () {
823
+ const stateRef = runtime.fiberState;
824
+ const currentState = yield* Ref.get(stateRef);
825
+ // Process deletions first
826
+ yield* Effect.forEach(currentState.deletions, (fiber) => Effect.gen(function* () {
827
+ const domParent = findDomParent(fiber);
828
+ if (Option.isSome(domParent)) {
829
+ yield* commitDeletion(fiber, domParent.value);
830
+ }
831
+ yield* deleteFiber(fiber);
832
+ }), { discard: true });
833
+ // Commit work starting from wipRoot.child
834
+ const firstChild = currentState.wipRoot.pipe(Option.flatMap((root) => root.child), Option.getOrUndefined);
835
+ if (firstChild) {
836
+ yield* commitWork(firstChild, runtime);
837
+ }
838
+ // Swap wipRoot to currentRoot
839
+ yield* Ref.update(stateRef, (s) => ({
840
+ ...s,
841
+ currentRoot: currentState.wipRoot,
842
+ wipRoot: Option.none(),
843
+ deletions: [],
844
+ }));
845
+ });
846
+ const commitWork = (fiber, runtime) => Effect.gen(function* () {
847
+ // KEY INSIGHT: If fiber has no DOM (function component), just process children
848
+ if (Option.isNone(fiber.dom)) {
849
+ if (Option.isSome(fiber.child)) {
850
+ yield* commitWork(fiber.child.value, runtime);
851
+ }
852
+ if (Option.isSome(fiber.sibling)) {
853
+ yield* commitWork(fiber.sibling.value, runtime);
854
+ }
855
+ return;
856
+ }
857
+ // Find DOM parent by walking up to nearest fiber with dom
858
+ const domParentOpt = findDomParent(fiber);
859
+ if (Option.isNone(domParentOpt)) {
860
+ // No DOM parent found - continue with children/siblings
861
+ if (Option.isSome(fiber.child)) {
862
+ yield* commitWork(fiber.child.value, runtime);
863
+ }
864
+ if (Option.isSome(fiber.sibling)) {
865
+ yield* commitWork(fiber.sibling.value, runtime);
866
+ }
867
+ return;
868
+ }
869
+ const domParent = domParentOpt.value;
870
+ // Process effect tag
871
+ const tag = fiber.effectTag.pipe(Option.getOrUndefined);
872
+ if (tag === "PLACEMENT") {
873
+ // Append DOM node
874
+ if (Option.isSome(fiber.dom)) {
875
+ domParent.appendChild(fiber.dom.value);
876
+ }
877
+ // Signal first child committed (for Suspense)
878
+ const deferred = fiber.parent.pipe(Option.flatMap((p) => p.childFirstCommitDeferred), Option.getOrUndefined);
879
+ if (deferred) {
880
+ const done = yield* Deferred.isDone(deferred);
881
+ if (!done) {
882
+ yield* Deferred.succeed(deferred, undefined);
883
+ }
884
+ }
885
+ }
886
+ else if (tag === "UPDATE") {
887
+ const prevProps = fiber.alternate.pipe(Option.map((alt) => alt.props), Option.getOrElse(() => ({})));
888
+ if (Option.isSome(fiber.dom)) {
889
+ yield* updateDom(fiber.dom.value, prevProps, fiber.props, fiber, runtime);
890
+ }
891
+ }
892
+ else if (tag === "DELETION") {
893
+ yield* commitDeletion(fiber, domParent);
894
+ return;
895
+ }
896
+ // Continue with children and siblings
897
+ if (Option.isSome(fiber.child)) {
898
+ yield* commitWork(fiber.child.value, runtime);
899
+ }
900
+ if (Option.isSome(fiber.sibling)) {
901
+ yield* commitWork(fiber.sibling.value, runtime);
902
+ }
903
+ });
904
+ // =============================================================================
905
+ // Work Loop
906
+ // =============================================================================
907
+ const workLoop = (runtime) => Effect.gen(function* () {
908
+ const stateRef = runtime.fiberState;
909
+ // Process all units of work using Effect.iterate for interruptibility
910
+ yield* Effect.iterate(yield* Ref.get(stateRef), {
911
+ while: (state) => Option.isSome(state.nextUnitOfWork),
912
+ body: (state) => Effect.gen(function* () {
913
+ const nextUnitOfWork = yield* performUnitOfWork(Option.getOrThrow(state.nextUnitOfWork), runtime);
914
+ yield* Ref.update(stateRef, (s) => ({ ...s, nextUnitOfWork }));
915
+ return yield* Ref.get(stateRef);
916
+ }),
917
+ });
918
+ // If we have a wipRoot but no more work, commit
919
+ const finalState = yield* Ref.get(stateRef);
920
+ if (Option.isNone(finalState.nextUnitOfWork) && Option.isSome(finalState.wipRoot)) {
921
+ yield* commitRoot(runtime);
922
+ }
923
+ });
924
+ // =============================================================================
925
+ // Main Render Function
926
+ // =============================================================================
927
+ /**
928
+ * Render a VElement tree to a DOM container using fiber-based reconciliation.
929
+ *
930
+ * This implementation:
931
+ * - Uses a fiber tree for efficient updates
932
+ * - Supports key-based diffing
933
+ * - Function components have no DOM wrapper - fixing SSR hydration
934
+ *
935
+ * @param element - VElement to render
936
+ * @param container - DOM container to render into
937
+ */
938
+ export const renderFiber = (element, container) => Effect.gen(function* () {
939
+ const runtime = yield* FibraeRuntime;
940
+ const stateRef = runtime.fiberState;
941
+ const currentState = yield* Ref.get(stateRef);
942
+ // Create root fiber with container as DOM
943
+ const rootFiber = createFiber(Option.none(), { children: [element] }, Option.none(), currentState.currentRoot, Option.none());
944
+ rootFiber.dom = Option.some(container);
945
+ yield* Ref.update(stateRef, (s) => ({
946
+ ...s,
947
+ wipRoot: Option.some(rootFiber),
948
+ deletions: [],
949
+ }));
950
+ const newState = yield* Ref.get(stateRef);
951
+ yield* Ref.update(stateRef, (s) => ({
952
+ ...s,
953
+ nextUnitOfWork: newState.wipRoot,
954
+ }));
955
+ yield* workLoop(runtime);
956
+ // Keep runtime alive
957
+ return yield* Effect.never;
958
+ });
959
+ // =============================================================================
960
+ // Hydration Support
961
+ // =============================================================================
962
+ /**
963
+ * Hydrate an existing DOM tree with a VElement tree.
964
+ *
965
+ * This walks the existing DOM and builds a fiber tree that matches it,
966
+ * enabling reactive updates without re-creating the DOM.
967
+ */
968
+ export const hydrateFiber = (element, container) => Effect.gen(function* () {
969
+ const runtime = yield* FibraeRuntime;
970
+ const stateRef = runtime.fiberState;
971
+ // Create root fiber with container as DOM
972
+ const rootFiber = createFiber(Option.none(), { children: [element] }, Option.none(), Option.none(), Option.none());
973
+ rootFiber.dom = Option.some(container);
974
+ // Build fiber tree by walking DOM and VElement together
975
+ // The element itself is the child of the root fiber (same as renderFiber)
976
+ yield* hydrateChildren(rootFiber, [element], Array.from(container.childNodes), runtime);
977
+ yield* Ref.update(stateRef, (s) => ({
978
+ ...s,
979
+ currentRoot: Option.some(rootFiber),
980
+ wipRoot: Option.none(),
981
+ deletions: [],
982
+ }));
983
+ // Keep runtime alive
984
+ return yield* Effect.never;
985
+ });
986
+ const hydrateChildren = (parentFiber, vElements, domNodes, runtime) => Effect.gen(function* () {
987
+ let domIndex = 0;
988
+ const fibers = [];
989
+ for (const vElement of vElements) {
990
+ const fiber = yield* hydrateElement(parentFiber, vElement, domNodes, domIndex, runtime);
991
+ fibers.push(fiber);
992
+ // Advance DOM index based on element type
993
+ if (typeof vElement.type === "string") {
994
+ domIndex++;
995
+ }
996
+ // Function components don't consume DOM nodes directly
997
+ }
998
+ // Link fibers
999
+ linkFibersAsSiblings(fibers, parentFiber);
1000
+ });
1001
+ const hydrateElement = (parentFiber, vElement, domNodes, domIndex, runtime) => Effect.gen(function* () {
1002
+ const fiber = createFiber(Option.some(vElement.type), vElement.props, Option.some(parentFiber), Option.none(), Option.none() // No effect tag - already in DOM
1003
+ );
1004
+ if (typeof vElement.type === "function") {
1005
+ // Function component - invoke to get children
1006
+ yield* hydrateFunctionComponent(fiber, vElement, domNodes, domIndex, runtime);
1007
+ }
1008
+ else if (vElement.type === "TEXT_ELEMENT") {
1009
+ // Text node
1010
+ const domNode = domNodes[domIndex];
1011
+ fiber.dom = Option.some(domNode);
1012
+ }
1013
+ else if (vElement.type === "FRAGMENT") {
1014
+ // Fragment - children are direct children of parent DOM
1015
+ yield* hydrateChildren(fiber, vElement.props.children || [], domNodes.slice(domIndex), runtime);
1016
+ }
1017
+ else {
1018
+ // Host element
1019
+ const domNode = domNodes[domIndex];
1020
+ fiber.dom = Option.some(domNode);
1021
+ // Inherit renderContext from parent fiber (function components capture it during render)
1022
+ if (Option.isNone(fiber.renderContext) && Option.isSome(fiber.parent)) {
1023
+ fiber.renderContext = fiber.parent.value.renderContext;
1024
+ }
1025
+ // Attach event listeners - uses runForkWithRuntime internally for full context
1026
+ attachEventListeners(domNode, vElement.props, runtime);
1027
+ // Handle ref
1028
+ const ref = vElement.props.ref;
1029
+ if (ref && typeof ref === "object" && "current" in ref) {
1030
+ ref.current = domNode;
1031
+ }
1032
+ // Hydrate children
1033
+ const childNodes = Array.from(domNode.childNodes);
1034
+ yield* hydrateChildren(fiber, vElement.props.children || [], childNodes, runtime);
1035
+ }
1036
+ return fiber;
1037
+ });
1038
+ const hydrateFunctionComponent = (fiber, vElement, domNodes, domIndex, runtime) => Effect.gen(function* () {
1039
+ // Capture current context during render phase for event handlers in commit phase
1040
+ const currentContext = (yield* FiberRef.get(FiberRef.currentContext));
1041
+ fiber.renderContext = Option.some(currentContext);
1042
+ // Set up atom tracking
1043
+ const accessedAtoms = new Set();
1044
+ const trackingRegistry = makeTrackingRegistry(runtime.registry, accessedAtoms);
1045
+ const contextWithTracking = Context.add(currentContext, AtomRegistry.AtomRegistry, trackingRegistry);
1046
+ // Invoke component
1047
+ const component = vElement.type;
1048
+ const output = yield* Effect.sync(() => component(vElement.props));
1049
+ fiber.isMultiEmissionStream = isStream(output);
1050
+ // Get first value from stream
1051
+ const stream = normalizeToStream(output).pipe(Stream.provideContext(contextWithTracking));
1052
+ // Create scope for this component
1053
+ yield* resubscribeFiber(fiber);
1054
+ fiber.accessedAtoms = Option.some(accessedAtoms);
1055
+ const scope = yield* getComponentScopeOrDie(fiber, "Expected componentScope");
1056
+ // Set up fiber ref
1057
+ const fiberRef = { current: fiber };
1058
+ fiber.fiberRef = Option.some(fiberRef);
1059
+ // Subscribe to component stream - errors typed via "fail" mode
1060
+ const firstValueDeferred = yield* subscribeComponentStream(stream, fiberRef, scope);
1061
+ // Wait for first value
1062
+ const childVElement = yield* Deferred.await(firstValueDeferred);
1063
+ fiber.latestStreamValue = Option.some(childVElement);
1064
+ // Hydrate the child VElement against remaining DOM nodes
1065
+ yield* hydrateChildren(fiber, [childVElement], domNodes.slice(domIndex), runtime);
1066
+ // Subscribe to atom changes
1067
+ yield* subscribeFiberAtoms(fiber, accessedAtoms, runtime);
1068
+ });
1069
+ //# sourceMappingURL=fiber-render.js.map